This document is an actionable, LLM-friendly playbook for building consistent, evolvable REST APIs
- 1. Core Principles
- 2. Protocol & Content
- 3. Resource Modeling & URLs
- 4. JSON Conventions
- 5. Pagination, Filtering, Sorting, Field Projection
- 6. Request Semantics
- 7. Error Model (Problem Details)
- 8. Concurrency & Idempotency
- 9. Authentication & Authorization
- 10. Rate Limiting & Quotas
- 11. Asynchronous Operations
- 12. Webhooks (Outbound)
- 13. Internationalization, Numbers & Time
- 14. Caching
- 15. Security & CORS
- 16. Observability & Diagnostics
- 17. Versioning & Deprecation
- 18. Canonical Status Codes
- 19. Batch & Bulk
- 20. OpenAPI & Codegen
- 21. Example Endpoints
- 22. Backward Compatibility Rules
- 23. Performance & DoS
- 24. Documentation Style
- Consistency over novelty: Prefer one clear way to do things.
- Explicitness: Always specify types, units, timezones, and defaults.
- Evolvability: Versioned paths, idempotency, and forward-compatible schemas.
- Observability: Every request traceable end-to-end.
- Security first: HTTPS only, least privilege, safe defaults.
- Media type:
application/json; charset=utf-8(request & response) - Errors: Problem Details
application/problem+json(RFC 9457) - Encoding: UTF-8
- Compression: gzip/br when client sends
Accept-Encoding - Idempotency:
Idempotency-Keyheader on unsafe methods (see §7)
- Nouns, plural:
/users,/tickets,/tickets/{ticket_id} - Hierarchy if strict ownership:
/users/{user_id}/keys - Prefer top-level + filters over deep nesting:
/tickets?assignee_id=... - Identifiers:
uuidv7(orulid). JSON field:id - Timestamps: ISO-8601 UTC with
Z, always include milliseconds.SSS(e.g.,2025-09-01T20:00:00.000Z) - Standard fields:
created_at,updated_at, optionaldeleted_at
- Naming: snake_case (consistent with backend conventions and databases)
- Nullability: Prefer omitting absent fields over
null - Booleans & enums: Strongly typed; never stringly booleans
- Money: Integer minor units + currency code
- Lists: Arrays; use
[]notnull - Envelope:
- Lists: Use
itemsarray with optional top-levelpage_infofor pagination - Single objects: Return fields directly at top level (no wrapper)
- Lists: Use
// List response
{
"items": [ /* array of objects */ ],
"page_info": { /* optional: limit, next_cursor, prev_cursor */ }
}
// Single object response (no wrapper)
{
"id": "01J...",
"title": "Example",
"created_at": "2025-09-01T20:00:00.000Z"
}For complete specification see QUERYING.md:
- Cursor pagination: Opaque, versioned cursors with
limitandcursorparameters - Filtering: OData
$filterwith operators (eq,ne,gt,in, etc.) - Sorting: OData
$orderby(e.g.,priority desc,created_at asc) - Field projection: OData
$selectfor sparse field selection (e.g.,$select=id,title,status)
- Create:
POST /tickets→ 201 +Location+ resource in body - Partial update:
PATCH /tickets/{id}(JSON Merge Patch) - Replace:
PUT /tickets/{id}(complete representation) - Delete:
DELETE /tickets/{id}→ 204; if soft-delete, return 200 withdeleted_at
- Always return RFC 9457 Problem Details for 4xx/5xx
{
"type": "https://api.example.com/errors/validation",
"title": "Invalid request",
"status": 422,
"detail": "email is invalid",
"instance": "https://api.example.com/req/01J...Z",
"errors": [
{ "field": "email", "code": "format", "message": "must be a valid email" }
],
"trace_id": "01J...Z"
}- Mappings: 422 (validation), 401/403 (authz), 404, 409 (conflict), 429, 5xx (no internals)
- Optimistic locking: Representations carry
ETag(strong or weak). Clients sendIf-Match. On mismatch → 412. - Idempotency: Clients may send
Idempotency-KeyonPOST/PATCH/DELETE.- Server caches only successful (2xx) responses to prevent duplicate side effects.
- Error responses (4xx/5xx) are NOT cached; retries re-execute to allow fresh validation, permission checks, and recovery from transient failures.
- Successful replays return the cached response with header:
Idempotency-Replayed: true. - Retention tiers:
- Minimum default: 1 hour (sufficient for network retry protection)
- Important operations: 24h-7d (e.g., notifications, reports) - must be documented per endpoint
- Critical operations: Permanent via DB uniqueness constraints (e.g., payments, user registration) → return
409 Conflictwith existing resource after initial creation
- Auth: OAuth2/OIDC Bearer tokens in
Authorization: Bearer <token> - Scopes/permissions: Document per endpoint; insufficient → 403
- Service-to-service: mTLS optional
- No secrets in URLs; short token TTLs; rotate keys; refresh tokens when needed
- Headers (following IETF RateLimit Draft):
RateLimit-Policy: "default";q=100;w=3600(defines quota policy: 100 requests per hour)RateLimit: "default";r=72;t=1800(current status: 72 remaining, resets in 1800 seconds)
- On 429 also include
Retry-After(seconds). Quotas are per token by default. - Example with partition key:
RateLimit-Policy: "peruser";q=100;w=60;pk=:dXNlcjEyMw==: - Backward compatibility: For legacy clients, servers MAY also return traditional
X-RateLimit-Limit,X-RateLimit-Remaining, andX-RateLimit-Resetheaders alongside the standard headers during a transition period. - Note: The IETF draft uses structured field syntax with parameters (not separate headers). Many existing APIs use
X-RateLimit-*headers, which remains acceptable for backward compatibility.
- For long tasks return
202 Accepted+Location: /jobs/{job_id} - Job resource example:
{
"id": "01J...",
"status": "queued|running|succeeded|failed|canceled",
"percent": 35,
"result": {},
"error": {},
"created_at": "...",
"updated_at": "..."
}- Clients poll
GET /jobs/{id}or subscribe via SSE/WebSocket if available
- Event shape:
event_type,id,created_at,data - Delivery: POST JSON to subscriber URL
- Security:
X-SignatureHMAC-SHA256 over raw body with shared secret; includeX-Timestamp(±5 min skew) - Retries: Exponential backoff for ≥24h; dead-letter queue
- Idempotency: Include
event_id; receivers dedupe
- All timestamps UTC (
Z), always include milliseconds.SSS(e.g.,2025-09-01T20:00:00.000Z). If timezone needed, add separatetimezone(IANA name) - JSON numbers for typical values; use strings for high-precision decimals or use integer minor units
- Sorting/filters are locale-agnostic unless documented otherwise
- Errors are not localized; localization is handled by the UI/API client.
- Reads:
ETag+Cache-Control: private, max-age=30when safe - Mutations:
Cache-Control: no-store - Conditional:
If-None-Match→ 304
- HTTPS only; HSTS enabled
- CORS allow-list explicit origins; example:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET,POST,PUT,PATCH,DELETE,OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type, Idempotency-Key
Access-Control-Expose-Headers: ETag, Location, RateLimit, RateLimit-Policy
- CSRF: only relevant for cookie auth; prefer Bearer in
Authorizationfor SPAs - Content Security Policy on the app domain; avoid wildcard
*/*
- Tracing: accept/propagate
traceparent(W3C). Emittrace_idheader on all responses - Request ID: honor
X-Request-Idor generate one - Structured logs: JSON per request:
trace_id,request_id,user_id,path,status,duration_ms,bytes - Metrics: RED/USE per route, with p50/p90/p99
- Path versioning:
/v1(breaking changes bump major) - Non-breaking changes: Adding optional fields/params, new endpoints, new enum values, relaxing validation
- Breaking changes: Removing fields, changing types/semantics, making optional fields required, changing URL structure
- Client compatibility: Must ignore unknown fields, handle new enum values gracefully, not rely on field order
- Deprecation headers:
Deprecation: true,Sunset: <RFC 8594 date>, andLink: <doc>; rel="deprecation"
For complete HTTP status code definitions and application error mappings, see STATUS_CODES.md.
Quick reference:
- 200 OK (read/update)
- 201 Created (+
Location) - 202 Accepted (async)
- 204 No Content (delete or idempotent update without body)
- 400 Bad Request
- 401 Unauthorized / 403 Forbidden
- 404 Not Found
- 409 Conflict
- 410 Gone (for deprecated endpoints)
- 412 Precondition Failed (ETag)
- 415 Unsupported Media Type
- 422 Unprocessable Entity (validation)
- 429 Too Many Requests
- 503 Service temporarily overloaded or under maintenance
- 5xx Other Server errors
For complete batch and bulk operations specification including error formats, atomicity options, and idempotency, see BATCH.md.
Quick Summary:
- Endpoint pattern:
POST /resources:batch(default maximum 100 items, configurable per endpoint) - Response:
207 Multi-Status(partial success) or specific status code (all same outcome) - Error format: Full RFC 9457 Problem Details per failed item
- Atomicity: Endpoint-specific (best-effort default, atomic for critical operations)
- Idempotency: Per-item
idempotency_keywith 1-hour retention - Optimistic locking: Per-item
if_matchfield for version checking
- Source of truth: OpenAPI 3.1
- For Rust backend specifics (utoipa, serde, validator), see
RUST.md - Client SDK: generate TypeScript types (
openapi-typescript) and React hooks (TanStack Query) with fetch/axios adapter - Keep schemas DRY via shared components; provide example payloads for every operation
- List Tickets
curl -sS \
-H "Authorization: Bearer $TOKEN" \
"https://api.example.com/v1/tickets?limit=25&cursor=...&\$filter=status in ('open','in_progress')&\$orderby=priority desc,created_at asc&\$select=id,title,priority,status,created_at"{
"items": [
{ "id": "01J...", "title": "Disk full", "priority": "high", "status": "open", "created_at": "2025-08-31T10:05:17.000Z" }
],
"page_info": {
"limit": 25,
"next_cursor": "eyJ2IjoxLCJrIjpbIjIwMjUtMDgtMzFUMTA6MDU6MTcuMDAwWiIsIjAxSi4uLiJdLCJvIjoiZGVzYyIsInMiOiJjcmVhdGVkX2F0LGlkIn0",
"prev_cursor": null
}
}- Update with Concurrency
PATCH /v1/tickets/01J...
If-Match: W/"etag-abc"
Idempotency-Key: 5b2f...
{ "status": "in_progress" }
- Async Job
POST /v1/reports → 202 Accepted
Location: /v1/jobs/01J...
- Clients ignore unknown fields
- Do not rely on property order
- Treat enums laxly: unknown enum → display as string, never crash
- Handle pagination cursors generically
- Enforce max list limits and payload sizes (e.g., 1MB JSON)
- Deny N+1 by default; allow explicit
include=with documented caps - Timeouts: handler ≤ 30s; use async jobs for longer work
- Strict input validation with precise 422s
Each endpoint must be comprehensively documented to serve both human developers and AI assistants.
-
Summary & Description
- One-line summary
- Detailed purpose explanation
- When to use this endpoint
-
Authentication & Authorization
- Required authentication method
- Required scopes/permissions
-
Request Specification
- HTTP method and path
- Path parameters (type, format, constraints)
- Query parameters (defaults, validation)
- Request headers (required and optional)
- Request body schema with field descriptions
-
Response Specification
- Success status codes
- Response headers
- Response body schema
- Example successful responses
-
Error Documentation
- All possible error status codes
- Problem Details examples for each
- Common error scenarios
-
Rate Limiting
- Rate limit class
- Quota consumption
-
Code Examples
- curl with realistic data
- TypeScript with generated client
Endpoint: POST /v1/resources
Purpose: Create new resource
Authentication: Required (OAuth2)
Authorization: resources:write scope
Rate Limit: Standard (100/hour)
Request Body Schema:
{
"title": "string (required, max 255 chars)",
"description": "string (optional, max 1000 chars)",
"priority": "enum: low|medium|high",
"category": "string (optional)"
}Success Response (201):
{
"id": "res_01JCXYZ...",
"title": "string",
"description": "string",
"priority": "medium",
"category": "string",
"status": "active",
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:30:00Z"
}Error Responses:
- 400: Invalid request body
- 401: Missing/invalid authentication
- 403: Insufficient permissions
- 422: Validation errors (Problem Details)
- 429: Rate limit exceeded
Code Examples:
curl -X POST https://api.example.com/v1/resources \
-H "Authorization: Bearer eyJ..." \
-H "Content-Type: application/json" \
-d '{"title": "Example", "priority": "medium"}'const resource = await api.createResource({
title: 'Example',
priority: 'medium'
});- Requests may include:
Authorization,Idempotency-Key,If-Match,If-None-Match,Accept-Encoding,traceparent,X-Request-Id - Responses should include:
Content-Type,ETag(when cacheable),Location(201/202),RateLimit,RateLimit-Policy,traceId,X-Request-Id
- RFC 9457 Problem Details: IETF RFC 9457
- IETF RateLimit Headers Draft: draft-ietf-httpapi-ratelimit-headers
- W3C Trace Context: traceparent