with one click
api-design
// Use when: designing or reviewing REST API endpoints, routes, request/response shapes, error formats, pagination, filtering, versioning, resource naming, or API architecture decisions.
// Use when: designing or reviewing REST API endpoints, routes, request/response shapes, error formats, pagination, filtering, versioning, resource naming, or API architecture decisions.
[HINT] Download the complete skill directory including SKILL.md and all related files
| name | api-design |
| description | Use when: designing or reviewing REST API endpoints, routes, request/response shapes, error formats, pagination, filtering, versioning, resource naming, or API architecture decisions. |
| user-invocable | false |
/user-profiles/:id/addresses). Use nouns for resources, verbs only for actions (/orders/:id/cancel).camelCase for JS/TS, snake_case for Rust/Go. Be consistent within a project./api/v1/products?type=voucher&category=digital-points). Do NOT use camelCase, snake_case, or other conventions for query filter names ā always kebab-case.Design APIs based on how consumers use the data. When page references are available (Figma, live URL via chrome-devtools MCP, or frontend source code), study the page structure first and let it guide your API shape. When no page reference is available, reason from the requirements about what data each use case needs.
List vs Detail separation: If the feature involves browsing items and viewing individual ones, split into list and detail endpoints. The list endpoint returns only the fields the list view needs (e.g., id, title, thumbnail, summary). The detail endpoint returns the full resource. Never return all detail fields in a list response.
Single purpose: Each endpoint should serve one clear purpose. Avoid god endpoints that accept many optional parameters and behave differently based on parameter combinations ā split them into semantically distinct endpoints.
Performance-driven splitting: Don't mix cheap and expensive operations. If a list page shows aggregate stats (counts, sums) that are expensive to compute, expose them as a separate stats endpoint so the client can fetch them in parallel without blocking the list.
sort=created_at&order=desc). Default to a sensible sort order based on the use case.{
"list": [],
"hasNext": true,
"anchor": "eyJpZCI6MTAwfQ"
}
<T>[])null when no more pages.COUNT(*) on large tables). Only add when the UI explicitly requires it (e.g., "showing 1-20 of 1,234 results").Write operations should reflect business actions, not just CRUD mappings. When the operation has clear domain semantics, use an action endpoint:
POST /orders/:id/cancel over PATCH /orders/:id { status: 'cancelled' }Idempotency-Key header. The server stores the key and returns the cached response on retries, preventing double-submit.POST /orders/:id/cancel) are naturally idempotent if the action is a state transition ā cancelling an already-cancelled order is a no-op, not an error./users/:id/addresses) for strong ownership. Use flat routes with query params (/posts?author-id=:id) for cross-cutting queries. Avoid nesting beyond two levels.Follow the project's existing error format. If none exists, use a flat structure:
{
"code": "0x0001",
"name": "VALIDATION_ERROR",
"message": "Email format is invalid",
"details": [
{ "field": "email", "message": "must be a valid email address" }
]
}
0x0000, 0x0001, ...) for programmatic matchingRules:
details array for validation errors ā one entry per invalid fieldmessage actionable ā tell the user what to fix, not what went wrong internallyUse status codes correctly ā don't default everything to 200 or 400.
Success:
200 ā OK (GET, PUT, PATCH, DELETE with response body)201 ā Created (POST that creates a resource)204 ā No Content (DELETE or action with no response body)Client errors:
400 ā Bad Request (malformed syntax, invalid JSON)401 ā Unauthorized (missing or invalid authentication)403 ā Forbidden (authenticated but insufficient permissions)404 ā Not Found (resource doesn't exist)409 ā Conflict (duplicate creation, invalid state transition)422 ā Unprocessable Entity (valid syntax but failed validation ā prefer over 400 for validation errors)Server errors:
500 ā Internal Server Error (unexpected failure ā never intentionally return this)502 ā Bad Gateway (upstream service returned an invalid response ā e.g., calling GCP Storage or a payment gateway that errors out)503 ā Service Unavailable (server temporarily unable to handle requests ā dependency down, overloaded). Use Retry-After header when possible.Use Unix epoch milliseconds (13 digits) for all timestamp fields. This keeps the wire format timezone-neutral, compact, and easy for common clients to parse.
{
"createdAt": 1709472000000,
"updatedAt": 1709558400000
}
Store as UTC. Timezone display is the frontend's responsibility.
null ā field is present, value is empty. Use for optional fields that have no value.For PATCH requests:
null means "clear this field"See language skills for serialization behavior that affects null vs omitted fields.
For long-running tasks (video transcoding, report generation, bulk imports), don't block the request:
202 Accepted immediately with a task reference{
"taskId": "task_abc123",
"status": "pending",
"statusUrl": "/tasks/task_abc123"
}
GET /tasks/task_abc123
ā { "status": "processing", "progress": 65 }
ā { "status": "completed", "result": { ... } }
ā { "status": "failed", "error": { ... } }
Use polling for simplicity. If the project already has WebSocket or webhook infrastructure, those are valid alternatives for push-based notification.
Use URL path versioning (/v1/users) when breaking changes are unavoidable. For internal APIs consumed by your own frontend, versioning is usually unnecessary ā just update both sides together. Reserve versioning for public/partner APIs with external consumers you can't coordinate with.
All stored data values (enum keys, category codes, status strings, type identifiers) must be in English. Never store display text or translated strings as data values. Translation is the frontend's responsibility ā the frontend maps English keys to localized strings via i18n or a key-label mapping.