api-design
SKILL.md
REST API Design (packages/functions)
For security patterns, see
securityskill
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
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
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
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
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
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)
// 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
Weekly Installs
2
Repository
trantuananh-17/…-reviewsFirst Seen
Jan 30, 2026
Security Audits
Installed on
cursor2
mistral-vibe2
qwen-code2
claude-code2
github-copilot2
codex2