Designs REST and GraphQL APIs following production best practices for resource naming, error handling, versioning, authentication, and documentation. Use this skill when designing new API endpoints, reviewing API contracts, implementing GraphQL schemas, establishing API conventions for a project, or writing API documentation. Apply when creating any route in Next.js API routes or route handlers, any Supabase Edge Function, or any backend endpoint — even if it starts small, these patterns prevent painful rewrites later.
Designs REST and GraphQL APIs following production best practices for resource naming, error handling, versioning, authentication, and documentation. Use this skill when designing new API endpoints, reviewing API contracts, implementing GraphQL schemas, establishing API conventions for a project, or writing API documentation. Apply when creating any route in Next.js API routes or route handlers, any Supabase Edge Function, or any backend endpoint — even if it starts small, these patterns prevent painful rewrites later.
API Design Principles
An API is a contract. Once external clients depend on it, changing it is expensive. Getting the contract right from the start — consistent naming, predictable errors, proper versioning, and accurate documentation — saves far more time than writing the code itself.
Workflow
Define the resource — what noun does this API expose?
Map operations to HTTP verbs — what actions do clients need?
Design the request/response shapes — agree on the contract before writing code
Define error cases — what can go wrong and how will it be communicated?
Document with OpenAPI — the spec is the source of truth
Implement — the code follows the spec, not the other way around
Output the API design to docs/api/YYYY-MM-DD-[resource]-api.md before implementation.
REST Design
Resource Naming
Resources are nouns, always plural. URLs identify resources; HTTP verbs describe actions on them.
GET /users — list users
GET /users/:id — get one user
POST /users — create a user
PUT /users/:id — replace a user
PATCH /users/:id — partial update
DELETE /users/:id — delete a user
Nested resources for belongsTo relationships:
GET /users/:userId/orders — user's orders
POST /users/:userId/orders — create order for user
GET /users/:userId/orders/:id — specific order
Avoid verbs in URLs: /getUser, /createOrder, /deleteAccount are wrong. The verb is the HTTP method.
HTTP Status Codes
Use the right status code — don't always return 200.
Code
Meaning
When to use
200
OK
Successful GET, PATCH
201
Created
Successful POST that created a resource
204
No Content
Successful DELETE or action with no body
400
Bad Request
Validation error, malformed request
401
Unauthorized
Not authenticated
403
Forbidden
Authenticated but not permitted
404
Not Found
Resource doesn't exist
409
Conflict
Duplicate, stale optimistic lock
422
Unprocessable Entity
Business logic validation failure
429
Too Many Requests
Rate limit exceeded
500
Internal Server Error
Unexpected server error
Consistent Error Format
All errors return a consistent JSON shape. Clients can rely on a single error parsing pattern.
interfaceApiError {
error: string; // machine-readable code: "VALIDATION_ERROR"message: string; // human-readable: "Email is required"details?: Record<string, string[]>; // field-level errors for validationrequestId?: string; // for support correlation
}
// 422 response:{"error":"VALIDATION_ERROR","message":"Request validation failed","details":{"email":["Email is required","Must be a valid email address"],"name":["Name must be at least 2 characters"]}}
Pagination
Use cursor-based pagination for large, frequently-updated datasets. Offset-based pagination (?page=2&limit=10) is unreliable when items are added or removed between requests.
// Request:GET /api/users?cursor=eyJpZCI6MTAwfQ&limit=20// Response:
{
"data": [...],
"pagination": {
"cursor": "eyJpZCI6MTIwfQ", // opaque cursor for next page"hasMore": true,
"total": 847// optional, expensive to compute
}
}
Cursor is a base64-encoded pointer (e.g., the last item's ID or timestamp).
Filtering and Sorting
Accept filters and sort via query parameters. Keep it consistent.
GET /api/products?status=active&category=electronics
GET /api/orders?sort=createdAt:desc&sort=total:asc
GET /api/users?search=john&role=admin
Validate filter values server-side — never pass raw query params to database queries.
Versioning
Choose one versioning strategy and apply it consistently. URL-based is most explicit and easiest to debug.
URL-based (recommended for REST):
/api/v1/users
/api/v2/users
Header-based:
API-Version: 2024-01
Query parameter:
/api/users?version=2
Maintain old versions for at least 6 months after deprecation. Communicate deprecations via response headers: Deprecation: Sun, 01 Jan 2025 00:00:00 GMT.
Authentication
Use Bearer tokens in the Authorization header. Never put tokens in URLs (they appear in server logs).
Batch and cache per-request data fetching with DataLoader. Without it, fetching 100 users' orders fires 100 separate queries.
importDataLoaderfrom'dataloader';
const orderLoader = newDataLoader(async (userIds: readonlystring[]) => {
const orders = await db.order.findMany({
where: { userId: { in: [...userIds] } },
});
// Return orders grouped by userId, in the same order as userIds:return userIds.map(id => orders.filter(o => o.userId === id));
});
// In User resolver:constUser = {
orders: (user: User) => orderLoader.load(user.id), // batched automatically
};
Error Handling in GraphQL
Use GraphQL errors for field-level failures; use HTTP 200 with errors array for partial failures; reserve non-200 HTTP responses for transport-level errors.
{"data":{"user":null},"errors":[{"message":"User not found","extensions":{"code":"NOT_FOUND","userId":"123"},"path":["user"]}]}
Validation and Security
Zod Schema Validation
Validate all request bodies and query parameters with Zod before processing. Never trust input.
Configure CORS restrictively — only allow origins that need access. Wildcard CORS (Access-Control-Allow-Origin: *) is appropriate only for truly public APIs.