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/createUser is 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_case in URLs and query params. Consistent with the JSON casing below.
- Sub-resources expose hierarchy, not actions.
/v1/orders/{id}/cancellation (PUT to create) instead of POST /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 |
PUT requires the full resource representation. A PUT with only some fields is a bug — that's PATCH's job.
PATCH body uses JSON merge patch (RFC 7396) — flat field-set means "change these, leave the rest". Don't invent your own dialect.
POST is 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
Deprecation and Sunset response 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=alice for free-text search across documented columns. Don't expose raw SQL LIKE patterns from clients.
- Sparse fieldsets:
?fields=id,email,created_at to 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:
type is 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_id required on 5xx so 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).
401 for missing/invalid credentials; 403 for 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:
12. Content negotiation & encoding
Content-Type: application/json; charset=utf-8 for request and response bodies.
Accept: application/json assumed; servers may return application/problem+json for errors regardless of Accept.
- 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.
null is intentional absence; missing field is "not provided" — these mean different things in PATCH. Document the distinction.
13. Caching
Cache-Control on every response. Defaults: private, no-store for authenticated user data; public, max-age=300 for genuinely public reference data (e.g. countries).
- ETag enables conditional GET (§9) — clients automatically reuse cached responses with a
304.
Vary: Authorization, Accept-Encoding when responses differ by these — without Vary, intermediaries serve the wrong cached entry to a different user.
14. Rate limiting
429 Too Many Requests when the limit is hit.
RateLimit-Limit / RateLimit-Remaining / RateLimit-Reset on every response; Retry-After on 429 and 503. Header reference in PAYLOADS § 4.
- Limit per caller (API key, user id), not per IP — IPs aren't reliable identity.
15. OpenAPI as the source of truth
- OpenAPI 3.1 spec is the contract. Every endpoint, every model, every error code, every header documented.
- Generated from the code, not hand-written — frameworks (FastAPI, gin-openapi, etc.) emit it from the route definitions. Hand-written specs rot the day after they ship.
- CI snapshot-tests the spec — diff against
openapi.snapshot.json on every PR; any change is reviewed.
- CI lints the spec with
redocly lint — catches missing descriptions, broken $refs, and style violations before the diff review even starts.
- Spec is published at a stable URL (e.g.
/v1/openapi.json) and consumed by client-SDK generators, Postman collections, and API documentation tooling.
- Rendered docs UI is project-dependent — Scalar is a good modern default (open-source, fast, themeable), but Redoc, Swagger UI, or a generated static site are equally valid depending on the project's constraints.
- Examples on every model and parameter. They drive the rendered docs and seed mock servers.
What this skill does NOT cover
- Framework specifics (
Depends, gin.Context, middleware ordering) — see the framework architect skills.
- Auth scheme deep dives (OAuth2 flows, OIDC, IdPs) — see the framework auth sections.
- Database access patterns — see sql-architect.
- HATEOAS / HAL / JSON-API — out of scope; this skill defines plain JSON REST.
1---2name: rest-api-architect3description: Cross-language REST conventions — resource URLs, method semantics, status codes, URL-prefix versioning, cursor pagination, snake_case JSON, ISO 8601 timestamps, RFC 7807 errors, Idempotency-Key, ETag/If-Match, OpenAPI as source of truth. Framework-agnostic. Use when designing or auditing REST endpoints.4---56# REST API Architecture78Cross-language conventions for HTTP/JSON REST APIs. Framework-agnostic. Pair with [fastapi-architect](../../frameworks/fastapi-architect/SKILL.md) or [gin-architect](../../frameworks/gin-architect/SKILL.md) for implementation. See [STACK.md](STACK.md) for the specs this skill is built on.910## 1. URL & resource design1112- **Plural nouns for collections:** `/v1/users`, `/v1/orders`. Never verbs in URLs (`/v1/createUser` is wrong — the method is the verb).13- **Resource ID in the path:** `/v1/users/{user_id}`, never as a query parameter.14- **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.15- **`snake_case` in URLs and query params.** Consistent with the JSON casing below.16- **Sub-resources expose hierarchy, not actions.** `/v1/orders/{id}/cancellation` (PUT to create) instead of `POST /v1/orders/{id}/cancel`. The state change is *what is created*, not a verb on the parent.1718## 2. HTTP method semantics1920| Method | Purpose | Idempotent | Safe |21|---|---|---|---|22| `GET` | Read | Yes | Yes (no side effects) |23| `POST` | Create (server assigns ID) **or** action that doesn't fit elsewhere | **No** | No |24| `PUT` | Replace entire resource (client supplies full state) | Yes | No |25| `PATCH` | Partial update | **No** (unless body is itself idempotent — usually not) | No |26| `DELETE` | Remove | Yes | No |2728- **`PUT` requires the full resource representation.** A `PUT` with only some fields is a bug — that's `PATCH`'s job.29- **`PATCH` body uses JSON merge patch** (RFC 7396) — flat field-set means "change these, leave the rest". Don't invent your own dialect.30- **`POST` is 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.3132## 3. Status codes3334Use 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](STATUS_CODES.md).3536**One hard rule:** never `200 OK` for errors. Returning `{"success": false, "error": ...}` with a 200 status is wrong and breaks every HTTP-aware tool.3738## 4. Versioning — URL prefix3940- **Path prefix only:** `/v1/users`, `/v2/users`. No header-based versioning, no query-param versioning.41- **Bump the version when a breaking change ships.** Additive changes (new optional field, new endpoint) stay in the same version.42- **Run versions side-by-side** until clients migrate. Deprecate with `Deprecation` and `Sunset` response headers (RFC 8594) before removing.43- **Internal microservices** can skip versioning until an external consumer appears — but it's cheap to start with `/v1/` from day one.4445## 5. Pagination — cursor, not offset4647Cursor 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](PAYLOADS.md#1-cursor-pagination).4849## 6. Filtering, sorting, searching5051- **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.52- **Sorting:** `?sort=-created_at,name` — comma-separated, prefix `-` for descending. Document allowed sort fields.53- **Searching:** `?q=alice` for free-text search across documented columns. Don't expose raw SQL `LIKE` patterns from clients.54- **Sparse fieldsets:** `?fields=id,email,created_at` to limit response payload — useful for list endpoints. Validate against the schema.5556## 7. Error contracts — RFC 7807 Problem Details5758Every error response uses `application/problem+json`. Canonical shape + rules in [PAYLOADS § 2](PAYLOADS.md#2-rfc-7807-problem-details); structured `422` validation shape in [PAYLOADS § 3](PAYLOADS.md#3-validation-errors-422).5960Key rules:6162- **`type` is a stable URL** — clients switch on it. Never change once published.63- **One shape for every error.** Don't mix RFC 7807 with framework defaults.64- **`correlation_id` required on `5xx`** so support can match server logs.6566## 8. Idempotency — `Idempotency-Key` mandatory6768Every `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.6970Full implementation reference — cache shape, TTL choice, concurrent-request handling, storage options, client guidance, common mistakes — in [IDEMPOTENCY.md](IDEMPOTENCY.md).7172## 9. Concurrency control — `ETag` + `If-Match` mandatory7374Every 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.7576Full implementation reference — where the ETag value comes from (default: monotonic version column), strong vs weak, the 412 retry flow, common mistakes — in [CONCURRENCY.md](CONCURRENCY.md).7778## 10. Auth & security headers7980- **Authentication via `Authorization: Bearer <token>`.** No tokens in query params (they leak into logs and referrer headers).81- **`401`** for missing/invalid credentials; **`403`** for valid credentials lacking permission. Mixing these confuses clients debugging access issues.82- **HTTPS always.** Reject plaintext in production at the load balancer; redirect at the edge.83- **CORS:** explicit allow-list of origins; never `Access-Control-Allow-Origin: *` for authenticated endpoints.84- **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`.85- **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.8687## 11. Authentication patterns8889Two 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](AUTH_PATTERNS.md).9091Framework-specific implementation:9293- [fastapi-architect §6](../../frameworks/fastapi-architect/SKILL.md#6-authentication--authorization) — `OAuth2PasswordBearer` + `pyjwt` + `argon2-cffi`94- [gin-architect §7](../../frameworks/gin-architect/SKILL.md#7-authentication--authorization) — `golang-jwt/jwt/v5` + `argon2`95- [nethttp-architect §8](../../frameworks/nethttp-architect/SKILL.md#8-authentication--authorization) — same Go libs, stdlib middleware shape9697## 12. Content negotiation & encoding9899- **`Content-Type: application/json; charset=utf-8`** for request and response bodies.100- **`Accept: application/json`** assumed; servers may return `application/problem+json` for errors regardless of `Accept`.101- **JSON field names: `snake_case`.** Matches Python and Go server conventions; client-side translation is trivial.102- **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.103- **Decimals as strings** for money (`"99.99"`) — JSON numbers are floats and lose precision.104- **UUIDs as canonical hex strings with dashes** (`"01j9x...-..."`). UUID v7 by default (sortable, distributed) — matches [sql-architect](../../databases/sql-architect/SKILL.md).105- **`null` is intentional absence; missing field is "not provided"** — these mean different things in `PATCH`. Document the distinction.106107## 13. Caching108109- **`Cache-Control` on every response.** Defaults: `private, no-store` for authenticated user data; `public, max-age=300` for genuinely public reference data (e.g. countries).110- **ETag enables conditional GET** (§9) — clients automatically reuse cached responses with a `304`.111- **`Vary: Authorization, Accept-Encoding`** when responses differ by these — without `Vary`, intermediaries serve the wrong cached entry to a different user.112113## 14. Rate limiting114115- **`429 Too Many Requests`** when the limit is hit.116- **`RateLimit-Limit` / `RateLimit-Remaining` / `RateLimit-Reset`** on every response; **`Retry-After`** on `429` and `503`. Header reference in [PAYLOADS § 4](PAYLOADS.md#4-rate-limit-headers).117- **Limit per *caller*** (API key, user id), not per IP — IPs aren't reliable identity.118119## 15. OpenAPI as the source of truth120121- **OpenAPI 3.1** spec is the contract. Every endpoint, every model, every error code, every header documented.122- **Generated from the code, not hand-written** — frameworks (FastAPI, gin-openapi, etc.) emit it from the route definitions. Hand-written specs rot the day after they ship.123- **CI snapshot-tests the spec** — diff against `openapi.snapshot.json` on every PR; any change is reviewed.124- **CI lints the spec with `redocly lint`** — catches missing descriptions, broken `$ref`s, and style violations before the diff review even starts.125- **Spec is published** at a stable URL (e.g. `/v1/openapi.json`) and consumed by client-SDK generators, Postman collections, and API documentation tooling.126- **Rendered docs UI is project-dependent** — [Scalar](https://github.com/scalar/scalar) is a good modern default (open-source, fast, themeable), but Redoc, Swagger UI, or a generated static site are equally valid depending on the project's constraints.127- **Examples on every model and parameter.** They drive the rendered docs and seed mock servers.128129## What this skill does NOT cover130131- **Framework specifics** (`Depends`, `gin.Context`, middleware ordering) — see the framework architect skills.132- **Auth scheme deep dives** (OAuth2 flows, OIDC, IdPs) — see the framework auth sections.133- **Database access patterns** — see [sql-architect](../../databases/sql-architect/SKILL.md).134- **HATEOAS / HAL / JSON-API** — out of scope; this skill defines plain JSON REST.