🎯

api-design

🎯Skill

from trantuananh-17/product-reviews

VibeIndex|
What it does

Designs and implements robust REST API endpoints with best practices, including response formatting, validation, error handling, and RESTful routing conventions.

πŸ“¦

Part of

trantuananh-17/product-reviews(5 items)

api-design

Installation

npm runRun npm script
npm run dev
πŸ“– Extracted from docs: trantuananh-17/product-reviews
1Installs
-
AddedFeb 4, 2026

Skill Details

SKILL.md

Use this skill when the user asks to "create an API endpoint", "build a REST API", "add a controller", "design an API", "implement CRUD operations", "add validation", "handle API errors", or any backend API development work. Provides REST API design patterns, response formats, validation, and best practices.

Overview

# REST API Design (packages/functions)

> For security patterns, see security skill

Directory Structure

```

packages/functions/src/

β”œβ”€β”€ routes/ # Route definitions

β”‚ β”œβ”€β”€ api.js # Admin API routes

β”‚ β”œβ”€β”€ restApiV2.js # Public REST API v2

β”‚ └── apiHookV1.js # Webhook routes

β”œβ”€β”€ controllers/ # Request handlers

β”œβ”€β”€ middleware/ # Auth, validation, rate limiting

β”œβ”€β”€ validations/ # Yup schemas

└── helpers/

└── restApiResponse.js # Response helpers

```

---

Response Format

Response Helpers

```javascript

import {

successResponse,

errorResponse,

paginatedResponse,

itemResponse

} from '../helpers/restApiResponse';

// Single item

ctx.body = itemResponse(customer);

// Paginated list

ctx.body = paginatedResponse(customers, pageInfo, total);

// Error

ctx.status = 400;

ctx.body = errorResponse('Invalid email', 'VALIDATION_ERROR', 400);

```

Response Structure

| Type | Format |

|------|--------|

| Success | {success: true, data, meta, timestamp} |

| Error | {success: false, error: {message, code, statusCode}, timestamp} |

| Paginated | {success: true, data: [], meta: {pagination: {...}}} |

---

HTTP Status Codes

| Code | When to Use |

|------|-------------|

| 200 | Successful GET, PUT |

| 201 | Successful POST (created) |

| 204 | Successful DELETE |

| 400 | Validation error, malformed request |

| 401 | Missing/invalid authentication |

| 403 | Authenticated but not authorized |

| 404 | Resource not found |

| 422 | Business logic error |

| 429 | Rate limit exceeded |

| 500 | Server error |

---

Route Design

RESTful Conventions

| Action | Method | Route |

|--------|--------|-------|

| List | GET | /resources |

| Get one | GET | /resources/:id |

| Create | POST | /resources |

| Update | PUT | /resources/:id |

| Delete | DELETE | /resources/:id |

| Action | POST | /resources/:id/action |

Route Organization

```javascript

import Router from 'koa-router';

const router = new Router({prefix: '/api/v2'});

router.use(verifyAuthenticate);

router.use(verifyPlanAccess);

// Resources

router.get('/customers', validateQuery(paginationSchema), getCustomers);

router.get('/customers/:id', getCustomer);

router.post('/customers', validateInput(createSchema), createCustomer);

router.put('/customers/:id', validateInput(updateSchema), updateCustomer);

// Sub-resources

router.get('/customers/:id/rewards', getCustomerRewards);

// Actions

router.post('/customers/:id/points/award', awardPoints);

```

---

Input Validation

Yup Schemas

```javascript

import * as Yup from 'yup';

export const createCustomerSchema = Yup.object({

email: Yup.string().email().required(),

firstName: Yup.string().max(100).optional(),

points: Yup.number().positive().optional()

});

export const paginationSchema = Yup.object({

limit: Yup.number().min(1).max(100).default(20),

cursor: Yup.string().optional()

});

```

Validation Middleware

```javascript

export function validateInput(schema) {

return async (ctx, next) => {

try {

ctx.request.body = await schema.validate(ctx.request.body, {

stripUnknown: true

});

await next();

} catch (error) {

ctx.status = 400;

ctx.body = errorResponse(error.message, 'VALIDATION_ERROR', 400);

}

};

}

```

---

Controller Pattern

```javascript

export async function getOne(ctx) {

try {

const {shop} = ctx.state;

const {id} = ctx.params;

const resource = await repository.getById(shop.id, id);

if (!resource) {

ctx.status = 404;

ctx.body = errorResponse('Not found', 'NOT_FOUND', 404);

return;

}

ctx.body = itemResponse(pick(resource, publicFields));

} catch (error) {

console.error('Error:', error);

ctx.status = 500;

ctx.body = errorResponse('Server error', 'INTERNAL_ERROR', 500);

}

}

```

---

Pagination

Cursor-Based (Preferred)

```javascript

// Request

GET /api/customers?limit=20&cursor=eyJpZCI6IjEyMyJ9

// Response

{

"data": [...],

"meta": {

"pagination": {

"hasNext": true,

"nextCursor": "eyJpZCI6IjE0MyJ9",

"limit": 20

}

}

}

```

---

Error Codes

| Code | When |

|------|------|

| UNAUTHORIZED | Missing/invalid credentials |

| FORBIDDEN | No permission |

| PLAN_RESTRICTED | Feature not in plan |

| VALIDATION_ERROR | Invalid input |

| NOT_FOUND | Resource doesn't exist |

| RATE_LIMITED | Too many requests |

| INTERNAL_ERROR | Server error |

---

Best Practices

| Do | Don't |

|----|-------|

| Use response helpers | Return raw objects |

| Set correct status codes | Return 200 for errors |

| Validate all inputs | Trust user input |

| Pick response fields | Expose internal fields |

| Scope queries by shopId | Query without shop filter |

| Use cursor pagination | Use offset at scale |

---

Checklist

```

β–‘ Uses response helpers (successResponse/errorResponse)

β–‘ Correct HTTP status codes

β–‘ Input validated with Yup schema

β–‘ Queries scoped by shopId

β–‘ Response fields picked (no internal data)

β–‘ Error handling with try-catch

β–‘ Rate limiting applied

β–‘ Authentication middleware

```