REST API Architecture
Cross-language conventions for HTTP/JSON REST APIs. Framework-agnostic. Pair with fastapi-architect or gin-architect for implementation. See STACK.md for the specs this skill is built on.
1. URL & resource design
- Plural nouns for collections:
/v1/users,/v1/orders. Never verbs in URLs (/v1/createUseris wrong — the method is the verb). - Resource ID in the path:
/v1/users/{user_id}, never as a query parameter. - Nest only one level deep for true containment:
/v1/orders/{order_id}/items. Beyond one level, use IDs and filter queries instead — deeper hierarchies turn brittle the moment relationships change. snake_casein URLs and query params. Consistent with the JSON casing below.- Sub-resources expose hierarchy, not actions.
/v1/orders/{id}/cancellation(PUT to create) instead ofPOST /v1/orders/{id}/cancel. The state change is what is created, not a verb on the parent.
2. HTTP method semantics
| Method | Purpose | Idempotent | Safe |
|---|---|---|---|
GET | Read | Yes | Yes (no side effects) |
POST | Create (server assigns ID) or action that doesn't fit elsewhere | No | No |
PUT | Replace entire resource (client supplies full state) | Yes | No |
PATCH | Partial update | No (unless body is itself idempotent — usually not) | No |
DELETE | Remove | Yes | No |
PUTrequires the full resource representation. APUTwith only some fields is a bug — that'sPATCH's job.PATCHbody uses JSON merge patch (RFC 7396) — flat field-set means "change these, leave the rest". Don't invent your own dialect.POSTis also for actions that don't map to CRUD — e.g./v1/payments/{id}/refunds(creates a refund). The resource is the action's outcome.
3. Status codes
Use the right code for the situation. The full table — every code, when to use it, the common confusions (401 vs 403, 404 vs 410, 422 vs 400) — is in STATUS_CODES.md.
One hard rule: never 200 OK for errors. Returning {"success": false, "error": ...} with a 200 status is wrong and breaks every HTTP-aware tool.
4. Versioning — URL prefix
- Path prefix only:
/v1/users,/v2/users. No header-based versioning, no query-param versioning. - Bump the version when a breaking change ships. Additive changes (new optional field, new endpoint) stay in the same version.
- Run versions side-by-side until clients migrate. Deprecate with
DeprecationandSunsetresponse headers (RFC 8594) before removing. - Internal microservices can skip versioning until an external consumer appears — but it's cheap to start with
/v1/from day one.
5. Pagination — cursor, not offset
Cursor pagination is stable under concurrent writes and O(1) per page; offset is O(N) and reads can shift between pages. Request/response shape + rules in PAYLOADS § 1.
6. Filtering, sorting, searching
- Filtering:
?status=paid&customer_id=01J9.... Equality only by default — operator syntax (?price[gte]=100) is fine for richer endpoints but document each operator in OpenAPI. - Sorting:
?sort=-created_at,name— comma-separated, prefix-for descending. Document allowed sort fields. - Searching:
?q=alicefor free-text search across documented columns. Don't expose raw SQLLIKEpatterns from clients. - Sparse fieldsets:
?fields=id,email,created_atto limit response payload — useful for list endpoints. Validate against the schema.
7. Error contracts — RFC 7807 Problem Details
Every error response uses application/problem+json. Canonical shape + rules in PAYLOADS § 2; structured 422 validation shape in PAYLOADS § 3.
Key rules:
typeis a stable URL — clients switch on it. Never change once published.- One shape for every error. Don't mix RFC 7807 with framework defaults.
correlation_idrequired on5xxso support can match server logs.
8. Idempotency — Idempotency-Key mandatory
Every POST and PATCH requires an Idempotency-Key header. Without it the server returns 400 Bad Request. The server caches the response keyed by (caller_id, method, path, key) for 24h and replays on retry; GET/PUT/DELETE are already idempotent by HTTP semantics and don't need it.
Full implementation reference — cache shape, TTL choice, concurrent-request handling, storage options, client guidance, common mistakes — in IDEMPOTENCY.md.
9. Concurrency control — ETag + If-Match mandatory
Every editable resource exposes an ETag on read. Every PUT and PATCH requires If-Match matching the current ETag, or returns 412 Precondition Failed. If-None-Match on GET enables 304 Not Modified caching for free.
Full implementation reference — where the ETag value comes from (default: monotonic version column), strong vs weak, the 412 retry flow, common mistakes — in CONCURRENCY.md.
10. Auth & security headers
- Authentication via
Authorization: Bearer <token>. No tokens in query params (they leak into logs and referrer headers). 401for missing/invalid credentials;403for valid credentials lacking permission. Mixing these confuses clients debugging access issues.- HTTPS always. Reject plaintext in production at the load balancer; redirect at the edge.
- CORS: explicit allow-list of origins; never
Access-Control-Allow-Origin: *for authenticated endpoints. - Security headers (added at the gateway or app layer):
Strict-Transport-Security,X-Content-Type-Options: nosniff,Content-Security-Policy(if serving HTML),Referrer-Policy: no-referrer. - No PII in URLs. Emails, names, IDs that map to PII go in headers or bodies — URLs end up in access logs, browser histories, and proxy caches.
11. Authentication patterns
Two patterns cover almost every API: in-house OAuth2 + JWT for single-service deployments, external IdP (Keycloak / Auth0 / Cognito / Entra) for multi-service, MFA, social login, or SSO/compliance needs. Full reference — Argon2id, HS256→RS256 switching, refresh-token rotation + reuse detection, JWKS verification with cached keys, mandatory aud/iss checks, when to switch A→B, per-endpoint authorization — in AUTH_PATTERNS.md.
Framework-specific implementation:
- fastapi-architect §6 —
OAuth2PasswordBearer+pyjwt+argon2-cffi - gin-architect §7 —
golang-jwt/jwt/v5+argon2 - nethttp-architect §8 — same Go libs, stdlib middleware shape
12. Content negotiation & encoding
Content-Type: application/json; charset=utf-8for request and response bodies.Accept: application/jsonassumed; servers may returnapplication/problem+jsonfor errors regardless ofAccept.- JSON field names:
snake_case. Matches Python and Go server conventions; client-side translation is trivial. - Timestamps: ISO 8601 with timezone, always as a string.
"2026-05-20T14:23:00Z"or"2026-05-20T14:23:00+00:00". Never Unix epoch numbers — they're ambiguous about units (seconds vs ms) and harder to log-grep. - Decimals as strings for money (
"99.99") — JSON numbers are floats and lose precision. - UUIDs as canonical hex strings with dashes (
"01j9x...-..."). UUID v7 by default (sortable, distributed) — matches sql-architect. nullis intentional absence; missing field is "not provided" — these mean different things inPATCH. Document the distinction.