API Design — Rules
1. Philosophy
- REST by default — Predictable, cacheable, tooling-friendly. GraphQL only when complexity justifies it.
- Design for consumers — Frontend-driven contracts. Version explicitly.
- Errors are first-class — Consistent format, actionable messages, traceable.
- Observability built-in — Request IDs, structured logs, metrics.
- Security by default — Auth, rate limiting, validation on every endpoint.
2. REST vs GraphQL
| Factor |
REST |
GraphQL |
| Use when |
Simple CRUD, caching critical, team familiarity |
Complex relations, multiple clients, evolving needs |
| Caching |
HTTP native (ETag, Cache-Control) |
Client-side (Apollo, Relay) |
| Over-fetching |
Common |
None |
| Under-fetching |
Multiple requests |
Single request |
| Learning curve |
Low |
Medium |
| Tooling |
Universal |
Specialized |
Default: REST — GraphQL only with team consensus and documented
justification.
3. Resource Naming
Conventions
# Collections (plural, kebab-case)
GET /api/users
POST /api/users
GET /api/users/{id}
PATCH /api/users/{id}
DELETE /api/users/{id}
# Nested resources
GET /api/users/{id}/posts
POST /api/users/{id}/posts
# Actions (avoid verbs in URL)
POST /api/users/{id}/activate # → PATCH /api/users/{id} { status: "active" }
POST /api/payments/{id}/refund # → POST /api/refunds { paymentId }
Rules
- Plural nouns —
/users not /user
- Kebab-case —
/user-profiles not /userProfiles
- No verbs — use HTTP methods
- Hierarchy max 2 levels —
/users/{id}/posts/{id}/comments OK, deeper → flatten
4. HTTP Methods
| Method |
Semantics |
Idempotent |
Body |
Response |
GET |
Retrieve |
✅ |
No |
200, 404 |
POST |
Create |
❌ |
Yes |
201, 400 |
PUT |
Replace (full) |
✅ |
Yes |
200, 404 |
PATCH |
Partial update |
✅ |
Yes |
200, 404 |
DELETE |
Delete |
✅ |
No |
204, 404 |
HEAD |
Metadata only |
✅ |
No |
200, 404 |
OPTIONS |
Capabilities |
✅ |
No |
200 |
Rules HTTP Methods
POST for create — returns 201 + Location header
PATCH for partial — PUT replaces entire resource
DELETE returns 204 — no body
- Idempotency keys for
POST/PATCH — Idempotency-Key header
5. Status Codes
| Code |
Use Case |
200 |
Success (GET, PUT, PATCH) |
201 |
Created (POST) — include Location |
204 |
No Content (DELETE, successful no-body) |
400 |
Bad Request — validation errors |
401 |
Unauthorized — missing/invalid auth |
403 |
Forbidden — valid auth, insufficient permissions |
404 |
Not Found |
409 |
Conflict — version mismatch, duplicate |
422 |
Unprocessable Entity — semantic errors |
429 |
Too Many Requests — rate limited |
500 |
Server Error — log with request ID |
503 |
Service Unavailable — maintenance, overload |
Rules Status
- Never return 200 with error body — use appropriate 4xx/5xx
- 401 vs 403 — 401 = "who are you?", 403 = "you can't do this"
- Include
requestId in all error responses for tracing
6. Error Handling
Format (RFC 9457 / Problem Details)
{
"type": "https://api.example.com/errors/validation-failed",
"title": "Validation failed",
"status": 422,
"detail": "Email already registered",
"instance": "/api/users",
"requestId": "req_abc123",
"errors": [
{
"field": "email",
"code": "already_exists",
"message": "Email already registered"
}
]
}
Required fields
| Field |
Required |
Description |
type |
✅ |
URI for error category |
title |
✅ |
Human-readable summary |
status |
✅ |
HTTP status code |
detail |
✅ |
Specific explanation |
instance |
✅ |
Request path |
requestId |
✅ |
Trace ID |
errors |
⚠️ |
Field-level (for 400/422) |
Rules Error Handling
- Consistent format — all errors follow this schema
- No stack traces in production
- Localized
title — accept Accept-Language header
requestId in response header — X-Request-Id
7. Pagination
Cursor-based (preferred)
GET /api/users?limit=20&after=eyJpZCI6MTAwfQ
{
"data": [...],
"pagination": {
"limit": 20,
"nextCursor": "eyJpZCI6MTIwfQ",
"hasMore": true
}
}
Offset (legacy only)
GET /api/users?page=2&limit=20
{
"data": [...],
"pagination": {
"page": 2,
"limit": 20,
"total": 150,
"totalPages": 8
}
}
Rules Pagination
- Cursor-based default — stable, performant, no skipped items
- Max limit 100 — enforce server-side
hasMore boolean — client doesn't calculate
- No negative offsets
8. Filtering, Sorting, Searching
Filtering
GET /api/users?filter[status]=active&filter[role]=admin
GET /api/users?filter[createdAt][$gte]=2024-01-01
Sorting
GET /api/users?sort=-createdAt,email
Searching
GET /api/users?q=john@example.com
Rules Filtering
filter[field][operator] — explicit operators ($eq, $gt,
$lt, $in, $like)
sort comma-separated — - prefix for desc
q for full-text — single search parameter
- Whitelist allowed fields — reject unknown
9. Versioning
URL versioning (preferred)
GET /api/v1/users
GET /api/v2/users
Header versioning (alternative)
GET /api/users
Accept: application/vnd.example.v2+json
Rules Versioning
- Major versions in URL —
/v1/, /v2/
- Minor versions backward compatible — additive only
- Deprecation header —
Deprecation: true, Sunset: Sat, 01 Jan 2025 00:00:00 GMT
- Support 2 versions max — deprecate after 12 months
10. Authentication
Auth patterns: see auth skill.
Summary
| Method |
Use Case |
| Bearer (JWT) |
Stateless, microservices |
| Session/Cookie |
SSR, traditional apps |
| API Key |
Server-to-server, webhooks |
Rules Authentication
Authorization: Bearer <token> for JWT
Cookie for sessions — HttpOnly, Secure, SameSite=Lax
- 401 with
WWW-Authenticate — Bearer realm="api"
11. Rate Limiting
Headers
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 99
X-RateLimit-Reset: 1704067200
Retry-After: 60
Response (429)
{
"type": "https://api.example.com/errors/rate-limited",
"title": "Rate limit exceeded",
"status": 429,
"detail": "Limit 100 requests per minute. Retry after 60 seconds.",
"retryAfter": 60
}
Rules Rate Limiting
- Per-IP + per-user — both layers
- Standard headers —
X-RateLimit-*, Retry-After
- Different limits — auth endpoints stricter (5/min), read generous (100/min)
- Return 429 with
Retry-After seconds
12. CORS
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PATCH, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization, Idempotency-Key
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 86400
Rules CORS
- Exact origin — no
* with credentials
- Preflight caching —
Max-Age 24h
- Allow only needed headers —
Authorization, Content-Type,
Idempotency-Key
13. HTTP Caching
Response headers
# Immutable (hashed assets)
Cache-Control: public, max-age=31536000, immutable
# Private (user-specific)
Cache-Control: private, max-age=0, must-revalidate
# Public with revalidation
Cache-Control: public, max-age=60, stale-while-revalidate=300
ETag: "abc123"
Last-Modified: Wed, 21 Oct 2024 07:28:00 GMT
Rules HTTP Caching
ETag + If-None-Match — prefer over Last-Modified
stale-while-revalidate — serve stale while revalidating
- No cache for auth/mutations —
private, no-store
14. Content Negotiation
Accept: application/json
Accept: application/vnd.example.v2+json
Accept-Language: en-US,en;q=0.9
Rules Content Negotiation
- Default:
application/json
- Version via
Accept header — application/vnd.example.v2+json
Accept-Language for localized errors
- 406 Not Acceptable if unsupported
15. OpenAPI
Minimal config
# openapi.yaml
openapi: 3.1.0
info:
title: Example API
version: 1.0.0
servers:
- url: https://api.example.com/v1
paths:
/users:
get:
summary: List users
parameters:
- $ref: "#/components/parameters/limit"
- $ref: "#/components/parameters/after"
responses:
"200":
description: Success
content:
application/json:
schema:
$ref: "#/components/schemas/UserList"
components:
schemas:
UserList:
type: object
properties:
data:
type: array
items: { $ref: "#/components/schemas/User" }
pagination:
$ref: "#/components/schemas/Pagination"
parameters:
limit:
name: limit
in: query
schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
after:
name: after
in: query
schema: { type: string }
Rules OpenAPI
- Contract-first — generate client/server from spec
- Single source —
openapi.yaml in repo
- CI validates — spec lint + breaking change detection
- Generate client —
openapi-typescript, orval
16. Dates and IDs
Dates
{ "createdAt": "2024-01-15T10:30:00Z" }
- ISO 8601 UTC — always
Z suffix
- No Unix timestamps in JSON
IDs
{ "id": "usr_abc123def456" }
- Prefixed ULIDs —
usr_, org_, pay_ (sortable, readable)
- No raw UUIDs — no prefix, not sortable
17. Methodology
Before using ANY API pattern not documented in this skill:
- MCP Context7 (priority):
context7_resolve-library-id + context7_query-docs for OpenAPI, REST patterns.
- Official docs: RESTful API standards, RFC 9457, HTTP specs.
- Project config:
openapi.yaml, API gateway config — verify against actual setup.
- HARD RULE: If not in this skill AND cannot be verified against 2 authoritative sources → DO NOT USE IT. Document as assumption or risk in report to orchestrator.
18. Prohibitions
- ❌ Do not use verbs in URLs (
/getUsers, /createUser)
- ❌ Do not return 200 with error payload
- ❌ Do not use
PUT for partial updates
- ❌ Do not use offset pagination for large datasets
- ❌ Do not expose internal IDs (DB PKs) — use prefixed ULIDs
- ❌ Do not skip
requestId in errors
- ❌ Do not use
* in CORS with credentials
- ❌ Do not version in query string (
?v=2)
- ❌ Do not break backward compatibility without deprecation period
19. References
Note: For JavaScript conventions (fetch, client), see JavaScript
Note: For TypeScript types, see TypeScript
Note: For Auth patterns, see Auth
Note: For Security (rate limiting, CORS), see Security
Last updated: 2026-08
1---2name: api-design3description: API design rules for modern frontend - REST vs GraphQL, naming, HTTP methods, status codes, versioning, auth, rate limiting, pagination, error handling, CORS, HTTP caching, OpenAPI, client architecture4---56# API Design — Rules78---910## 1. Philosophy11121. **REST by default** — Predictable, cacheable, tooling-friendly. GraphQL only when complexity justifies it.132. **Design for consumers** — Frontend-driven contracts. Version explicitly.143. **Errors are first-class** — Consistent format, actionable messages, traceable.154. **Observability built-in** — Request IDs, structured logs, metrics.165. **Security by default** — Auth, rate limiting, validation on every endpoint.1718---1920## 2. REST vs GraphQL2122| Factor | REST | GraphQL |23| ------------------ | ----------------------------------------------- | --------------------------------------------------- |24| **Use when** | Simple CRUD, caching critical, team familiarity | Complex relations, multiple clients, evolving needs |25| **Caching** | HTTP native (ETag, Cache-Control) | Client-side (Apollo, Relay) |26| **Over-fetching** | Common | None |27| **Under-fetching** | Multiple requests | Single request |28| **Learning curve** | Low | Medium |29| **Tooling** | Universal | Specialized |3031**Default: REST** — GraphQL only with team consensus and documented32justification.3334---3536## 3. Resource Naming3738### Conventions3940```text41# Collections (plural, kebab-case)42GET /api/users43POST /api/users44GET /api/users/{id}45PATCH /api/users/{id}46DELETE /api/users/{id}4748# Nested resources49GET /api/users/{id}/posts50POST /api/users/{id}/posts5152# Actions (avoid verbs in URL)53POST /api/users/{id}/activate # → PATCH /api/users/{id} { status: "active" }54POST /api/payments/{id}/refund # → POST /api/refunds { paymentId }55```5657### Rules5859- **Plural nouns** — `/users` not `/user`60- **Kebab-case** — `/user-profiles` not `/userProfiles`61- **No verbs** — use HTTP methods62- **Hierarchy max 2 levels** — `/users/{id}/posts/{id}/comments` OK, deeper → flatten6364---6566## 4. HTTP Methods6768| Method | Semantics | Idempotent | Body | Response |69| --------- | -------------- | ---------- | ---- | -------- |70| `GET` | Retrieve | ✅ | No | 200, 404 |71| `POST` | Create | ❌ | Yes | 201, 400 |72| `PUT` | Replace (full) | ✅ | Yes | 200, 404 |73| `PATCH` | Partial update | ✅ | Yes | 200, 404 |74| `DELETE` | Delete | ✅ | No | 204, 404 |75| `HEAD` | Metadata only | ✅ | No | 200, 404 |76| `OPTIONS` | Capabilities | ✅ | No | 200 |7778### Rules HTTP Methods7980- **`POST` for create** — returns 201 + `Location` header81- **`PATCH` for partial** — `PUT` replaces entire resource82- **`DELETE` returns 204** — no body83- **Idempotency keys** for `POST`/`PATCH` — `Idempotency-Key` header8485---8687## 5. Status Codes8889| Code | Use Case |90| ----- | ------------------------------------------------ |91| `200` | Success (GET, PUT, PATCH) |92| `201` | Created (POST) — include `Location` |93| `204` | No Content (DELETE, successful no-body) |94| `400` | Bad Request — validation errors |95| `401` | Unauthorized — missing/invalid auth |96| `403` | Forbidden — valid auth, insufficient permissions |97| `404` | Not Found |98| `409` | Conflict — version mismatch, duplicate |99| `422` | Unprocessable Entity — semantic errors |100| `429` | Too Many Requests — rate limited |101| `500` | Server Error — log with request ID |102| `503` | Service Unavailable — maintenance, overload |103104### Rules Status105106- **Never return 200 with error body** — use appropriate 4xx/5xx107- **401 vs 403** — 401 = "who are you?", 403 = "you can't do this"108- **Include `requestId`** in all error responses for tracing109110---111112## 6. Error Handling113114### Format (RFC 9457 / Problem Details)115116```json117{118 "type": "https://api.example.com/errors/validation-failed",119 "title": "Validation failed",120 "status": 422,121 "detail": "Email already registered",122 "instance": "/api/users",123 "requestId": "req_abc123",124 "errors": [125 {126 "field": "email",127 "code": "already_exists",128 "message": "Email already registered"129 }130 ]131}132```133134### Required fields135136| Field | Required | Description |137| ----------- | -------- | ------------------------- |138| `type` | ✅ | URI for error category |139| `title` | ✅ | Human-readable summary |140| `status` | ✅ | HTTP status code |141| `detail` | ✅ | Specific explanation |142| `instance` | ✅ | Request path |143| `requestId` | ✅ | Trace ID |144| `errors` | ⚠️ | Field-level (for 400/422) |145146### Rules Error Handling147148- **Consistent format** — all errors follow this schema149- **No stack traces** in production150- **Localized `title`** — accept `Accept-Language` header151- **`requestId` in response header** — `X-Request-Id`152153---154155## 7. Pagination156157### Cursor-based (preferred)158159```http160GET /api/users?limit=20&after=eyJpZCI6MTAwfQ161```162163```json164{165 "data": [...],166 "pagination": {167 "limit": 20,168 "nextCursor": "eyJpZCI6MTIwfQ",169 "hasMore": true170 }171}172```173174### Offset (legacy only)175176```http177GET /api/users?page=2&limit=20178```179180```json181{182 "data": [...],183 "pagination": {184 "page": 2,185 "limit": 20,186 "total": 150,187 "totalPages": 8188 }189}190```191192### Rules Pagination193194- **Cursor-based default** — stable, performant, no skipped items195- **Max limit 100** — enforce server-side196- **`hasMore` boolean** — client doesn't calculate197- **No negative offsets**198199---200201## 8. Filtering, Sorting, Searching202203### Filtering204205```http206GET /api/users?filter[status]=active&filter[role]=admin207GET /api/users?filter[createdAt][$gte]=2024-01-01208```209210### Sorting211212```http213GET /api/users?sort=-createdAt,email214```215216### Searching217218```http219GET /api/users?q=john@example.com220```221222### Rules Filtering223224- **`filter[field][operator]`** — explicit operators (`$eq`, `$gt`,225 `$lt`, `$in`, `$like`)226- **`sort` comma-separated** — `-` prefix for desc227- **`q` for full-text** — single search parameter228- **Whitelist allowed fields** — reject unknown229230---231232## 9. Versioning233234### URL versioning (preferred)235236```http237GET /api/v1/users238GET /api/v2/users239```240241### Header versioning (alternative)242243```http244GET /api/users245Accept: application/vnd.example.v2+json246```247248### Rules Versioning249250- **Major versions in URL** — `/v1/`, `/v2/`251- **Minor versions backward compatible** — additive only252- **Deprecation header** — `Deprecation: true`, `Sunset: Sat, 01 Jan 2025 00:00:00 GMT`253- **Support 2 versions max** — deprecate after 12 months254255---256257## 10. Authentication258259> **Auth patterns**: see `auth` skill.260261### Summary262263| Method | Use Case |264| ------------------ | -------------------------- |265| **Bearer (JWT)** | Stateless, microservices |266| **Session/Cookie** | SSR, traditional apps |267| **API Key** | Server-to-server, webhooks |268269### Rules Authentication270271- **`Authorization: Bearer <token>`** for JWT272- **`Cookie`** for sessions — `HttpOnly`, `Secure`, `SameSite=Lax`273- **401 with `WWW-Authenticate`** — `Bearer realm="api"`274275---276277## 11. Rate Limiting278279### Headers280281```http282X-RateLimit-Limit: 100283X-RateLimit-Remaining: 99284X-RateLimit-Reset: 1704067200285Retry-After: 60286```287288### Response (429)289290```json291{292 "type": "https://api.example.com/errors/rate-limited",293 "title": "Rate limit exceeded",294 "status": 429,295 "detail": "Limit 100 requests per minute. Retry after 60 seconds.",296 "retryAfter": 60297}298```299300### Rules Rate Limiting301302- **Per-IP + per-user** — both layers303- **Standard headers** — `X-RateLimit-*`, `Retry-After`304- **Different limits** — auth endpoints stricter (5/min), read generous (100/min)305- **Return 429** with `Retry-After` seconds306307---308309## 12. CORS310311```http312Access-Control-Allow-Origin: https://app.example.com313Access-Control-Allow-Methods: GET, POST, PATCH, DELETE, OPTIONS314Access-Control-Allow-Headers: Content-Type, Authorization, Idempotency-Key315Access-Control-Allow-Credentials: true316Access-Control-Max-Age: 86400317```318319### Rules CORS320321- **Exact origin** — no `*` with credentials322- **Preflight caching** — `Max-Age` 24h323- **Allow only needed headers** — `Authorization`, `Content-Type`,324 `Idempotency-Key`325326---327328## 13. HTTP Caching329330### Response headers331332```http333# Immutable (hashed assets)334Cache-Control: public, max-age=31536000, immutable335336# Private (user-specific)337Cache-Control: private, max-age=0, must-revalidate338339# Public with revalidation340Cache-Control: public, max-age=60, stale-while-revalidate=300341ETag: "abc123"342Last-Modified: Wed, 21 Oct 2024 07:28:00 GMT343```344345### Rules HTTP Caching346347- **`ETag` + `If-None-Match`** — prefer over `Last-Modified`348- **`stale-while-revalidate`** — serve stale while revalidating349- **No cache for auth/mutations** — `private, no-store`350351---352353## 14. Content Negotiation354355```http356Accept: application/json357Accept: application/vnd.example.v2+json358Accept-Language: en-US,en;q=0.9359```360361### Rules Content Negotiation362363- **Default: `application/json`**364- **Version via `Accept` header** — `application/vnd.example.v2+json`365- **`Accept-Language`** for localized errors366- **406 Not Acceptable** if unsupported367368---369370## 15. OpenAPI371372### Minimal config373374```yaml375# openapi.yaml376openapi: 3.1.0377info:378 title: Example API379 version: 1.0.0380servers:381 - url: https://api.example.com/v1382paths:383 /users:384 get:385 summary: List users386 parameters:387 - $ref: "#/components/parameters/limit"388 - $ref: "#/components/parameters/after"389 responses:390 "200":391 description: Success392 content:393 application/json:394 schema:395 $ref: "#/components/schemas/UserList"396components:397 schemas:398 UserList:399 type: object400 properties:401 data:402 type: array403 items: { $ref: "#/components/schemas/User" }404 pagination:405 $ref: "#/components/schemas/Pagination"406 parameters:407 limit:408 name: limit409 in: query410 schema: { type: integer, minimum: 1, maximum: 100, default: 20 }411 after:412 name: after413 in: query414 schema: { type: string }415```416417### Rules OpenAPI418419- **Contract-first** — generate client/server from spec420- **Single source** — `openapi.yaml` in repo421- **CI validates** — spec lint + breaking change detection422- **Generate client** — `openapi-typescript`, `orval`423424---425426## 16. Dates and IDs427428### Dates429430```json431{ "createdAt": "2024-01-15T10:30:00Z" }432```433434- **ISO 8601 UTC** — always `Z` suffix435- **No Unix timestamps** in JSON436437### IDs438439```json440{ "id": "usr_abc123def456" }441```442443- **Prefixed ULIDs** — `usr_`, `org_`, `pay_` (sortable, readable)444- **No raw UUIDs** — no prefix, not sortable445446---447448## 17. Methodology449450Before using ANY API pattern not documented in this skill:4514521. **MCP Context7** (priority): `context7_resolve-library-id` + `context7_query-docs` for OpenAPI, REST patterns.4532. **Official docs**: RESTful API standards, RFC 9457, HTTP specs.4543. **Project config**: `openapi.yaml`, API gateway config — verify against actual setup.4554. **HARD RULE**: If not in this skill AND cannot be verified against 2 authoritative sources → DO NOT USE IT. Document as assumption or risk in report to orchestrator.456457---458459## 18. Prohibitions460461- ❌ Do not use verbs in URLs (`/getUsers`, `/createUser`)462- ❌ Do not return 200 with error payload463- ❌ Do not use `PUT` for partial updates464- ❌ Do not use offset pagination for large datasets465- ❌ Do not expose internal IDs (DB PKs) — use prefixed ULIDs466- ❌ Do not skip `requestId` in errors467- ❌ Do not use `*` in CORS with credentials468- ❌ Do not version in query string (`?v=2`)469- ❌ Do not break backward compatibility without deprecation period470471---472473## 19. References474475> **Note:** For JavaScript conventions (fetch, client), see [JavaScript](../javascript/SKILL.md)476> **Note:** For TypeScript types, see [TypeScript](../typescript/SKILL.md)477> **Note:** For Auth patterns, see [Auth](../auth/SKILL.md)478> **Note:** For Security (rate limiting, CORS), see [Security](../security/SKILL.md)479480---481482Last updated: 2026-08