| name | api-design |
| description | REST API design patterns including resource naming, status codes, pagination, filtering, error responses, versioning, and rate limiting for production APIs. |
API Design Patterns
Conventions and best practices for designing consistent, developer-friendly REST APIs.
When to Activate
- Before writing any implementation — design the contract first, code second
- Designing new API endpoints or changing existing ones
- Reviewing existing API contracts for consistency
- Adding pagination, filtering, or sorting — see skill
api-pagination-filtering
- Implementing error handling for APIs
- Planning API versioning strategy
- Building public or partner-facing APIs
For the full Contract-First workflow (spec writing, code generation, CI breaking-change detection, Pact): see skill api-contract.
For API documentation production — platform choice (Mintlify, Docusaurus, Redoc, Scalar), OpenAPI descriptions/examples, interactive playground, changelog automation, Vale prose linting, and Divio structure: see skill api-docs-patterns.
Contract-First Principle
Write the OpenAPI spec before writing any implementation code.
@startuml
:Write spec\napi/v1/openapi.yaml;
note right
REST → OpenAPI 3.1
Events → AsyncAPI 3.0
end note
:Lint spec\nspectral lint;
:Generate types / stubs\nfrom spec;
note right
TS: openapi-typescript
Go: oapi-codegen
Java: openapi-generator
Python: datamodel-codegen
end note
:Implement business logic only;
:Validate in CI\nschemathesis + oasdiff;
@enduml
The spec is the public contract. Consumers depend on it. Code is a private implementation detail.
- Never generate the spec from code (annotations, reflection) — it will drift
- Never write request/response types by hand — generate them from the spec
- Any breaking change requires a new API version (
/api/v2/)
See skill api-contract for the complete toolchain and CI setup.
Write descriptions and examples from the start
OpenAPI documentation is easiest to write while you are designing the spec — not after the implementation is shipped.
Minimum documentation requirements per operation (add these when you write each path, not later):
paths:
/orders:
post:
summary: Create an order
description: |
Places a new order for the authenticated customer.
The order is created in `pending` status and transitions to
`processing` once payment is confirmed (async, webhook fired).
**Scopes required:** `orders:write`
operationId: createOrder
tags: [Orders]
parameters: []
requestBody:
content:
application/json:
schema:
{ }
{ }
See skill api-docs-patterns for the full documentation workflow: platform setup, interactive playground, code examples in all languages, changelog automation, and CI.
Resource Design
URL Structure
# Resources are nouns, plural, lowercase, kebab-case
GET /api/v1/users
GET /api/v1/users/:id
POST /api/v1/users
PUT /api/v1/users/:id
PATCH /api/v1/users/:id
DELETE /api/v1/users/:id
# Sub-resources for relationships
GET /api/v1/users/:id/orders
POST /api/v1/users/:id/orders
# Actions that don't map to CRUD (use verbs sparingly)
POST /api/v1/orders/:id/cancel
POST /api/v1/auth/login
POST /api/v1/auth/refresh
Naming Rules
# GOOD
/api/v1/team-members # kebab-case for multi-word resources
/api/v1/orders?status=active # query params for filtering
/api/v1/users/123/orders # nested resources for ownership
# BAD
/api/v1/getUsers # verb in URL
/api/v1/user # singular (use plural)
/api/v1/team_members # snake_case in URLs
/api/v1/users/123/getOrders # verb in nested resource
HTTP Methods and Status Codes
Method Semantics
| Method | Idempotent | Safe | Use For |
|---|
| GET | Yes | Yes | Retrieve resources |
| POST | No | No | Create resources, trigger actions |
| PUT | Yes | No | Full replacement of a resource |
| PATCH | No* | No | Partial update of a resource |
| DELETE | Yes | No | Remove a resource |
*PATCH can be made idempotent with proper implementation
Status Code Reference
# Success
200 OK — GET, PUT, PATCH (with response body)
201 Created — POST (include Location header)
204 No Content — DELETE, PUT (no response body)
# Client Errors
400 Bad Request — Validation failure, malformed JSON
401 Unauthorized — Missing or invalid authentication
403 Forbidden — Authenticated but not authorized
404 Not Found — Resource doesn't exist
409 Conflict — Duplicate entry, state conflict
422 Unprocessable Entity — Semantically invalid (valid JSON, bad data)
429 Too Many Requests — Rate limit exceeded
# Server Errors
500 Internal Server Error — Unexpected failure (never expose details)
502 Bad Gateway — Upstream service failed
503 Service Unavailable — Temporary overload, include Retry-After
Common Mistakes
# BAD: 200 for everything
{ "status": 200, "success": false, "error": "Not found" }
# GOOD: Use HTTP status codes semantically + RFC 7807 body
HTTP/1.1 404 Not Found
Content-Type: application/problem+json
{
"type": "https://api.example.com/problems/not-found",
"title": "Not Found",
"status": 404,
"detail": "User abc-123 not found.",
"instance": "/api/v1/users/abc-123"
}
# BAD: 500 for validation errors
# GOOD: 400 or 422 with field-level details (RFC 7807 errors extension)
# BAD: 200 for created resources
# GOOD: 201 with Location header
HTTP/1.1 201 Created
Location: /api/v1/users/abc-123
Response Format
Success Response
{
"data": {
"id": "abc-123",
"email": "alice@example.com",
"name": "Alice",
"created_at": "2025-01-15T10:30:00Z"
}
}
Collection Response (with Pagination)
{
"data": [
{ "id": "abc-123", "name": "Alice" },
{ "id": "def-456", "name": "Bob" }
],
"meta": {
"total": 142,
"page": 1,
"per_page": 20,
"total_pages": 8
},
"links": {
"self": "/api/v1/users?page=1&per_page=20",
"next": "/api/v1/users?page=2&per_page=20",
"last": "/api/v1/users?page=8&per_page=20"
Error Response — RFC 7807 / RFC 9457 Problem Details
All error responses MUST use Content-Type: application/problem+json and the standard fields:
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json
{
"type": "https://api.example.com/problems/validation-failed",
"title": "Validation Failed",
"status": 400,
"detail": "One or more fields failed validation.",
"instance": "/api/v1/users",
"errors": [
{ "field": "email", "detail": "must be a valid email address" },
{ "field": "age", "detail": "must be between 0 and 150" }
]
}
| Field | Required | Description |
|---|
type | Recommended | URI identifying the problem type. about:blank if no docs exist yet. |
title | Recommended | Stable, human-readable summary (don't interpolate dynamic data). |
status | Yes | HTTP status code mirrored in the body. |
detail | Optional | Occurrence-specific explanation for the client. |
instance | Optional | URI of this specific occurrence (e.g., request path or ID). |
errors | Extension | RFC 9457 array for multiple sub-problems (validation errors). |
See skill: problem-details for full specification and per-language implementation.
Response Envelope Variants
interface ApiResponse<T> {
data: T;
meta?: PaginationMeta;
links?: PaginationLinks;
}
interface ProblemDetails {
type: string;
title: string;
status: number;
detail?: string;
instance?: string;
[key: string]: unknown;
}
For pagination (offset/cursor), filtering, sorting, and sparse fieldsets — see skill api-pagination-filtering.
Authentication and Authorization
Token-Based Auth
# Bearer token in Authorization header
GET /api/v1/users
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
# API key (for server-to-server)
GET /api/v1/data
X-API-Key: sk_live_abc123
Authorization Patterns
app.get("/api/v1/orders/:id", async (req, res) => {
const order = await Order.findById(req.params.id);
if (!order) return res.status(404).json({ error: { code: "not_found" } });
if (order.userId !== req.user.id) return res.status(403).json({ error: { code: "forbidden" } });
return res.json({ data: order });
});
app.delete("/api/v1/users/:id", requireRole("admin"), async (req, res) => {
await User.delete(req.params.id);
return res.status(204).send();
});
Rate Limiting
Headers
HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1640000000
# When exceeded — RFC 7807 body
HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
Retry-After: 60
{
"type": "https://api.example.com/problems/too-many-requests",
"title": "Too Many Requests",
"status": 429,
"detail": "Rate limit exceeded. Try again in 60 seconds.",
"retryAfter": 60
}
Rate Limit Tiers
| Tier | Limit | Window | Use Case |
|---|
| Anonymous | 30/min | Per IP | Public endpoints |
| Authenticated | 100/min | Per user | Standard API access |
| Premium | 1000/min | Per API key | Paid API plans |
| Internal | 10000/min | Per service | Service-to-service |
Versioning
URL Path Versioning (Recommended)
/api/v1/users
/api/v2/users
Pros: Explicit, easy to route, cacheable
Cons: URL changes between versions
Header Versioning
GET /api/users
Accept: application/vnd.myapp.v2+json
Pros: Clean URLs
Cons: Harder to test, easy to forget
Versioning Strategy
1. Start with /api/v1/ — don't version until you need to
2. Maintain at most 2 active versions (current + previous)
3. Deprecation timeline:
- Announce deprecation (6 months notice for public APIs)
- Add Sunset header: Sunset: Sat, 01 Jan 2026 00:00:00 GMT
- Return 410 Gone after sunset date
4. Non-breaking changes don't need a new version:
- Adding new fields to responses
- Adding new optional query parameters
- Adding new endpoints
5. Breaking changes require a new version:
- Removing or renaming fields
- Changing field types
- Changing URL structure
- Changing authentication method
For per-language implementation patterns (TypeScript, Go), anti-patterns, and the full API Design Checklist, see skill api-design-advanced.