Resources, not actions
REST models "things" as resources identified by URIs, and uses HTTP methods (GET/POST/PUT/PATCH/DELETE) for operations. Counter-examples put verbs in the path, like /getUser or /createOrder, mixing RPC style into REST.
Common conventions
| Method | Meaning | Idempotent |
|---|---|---|
| GET | Read | Yes |
| POST | Create | No |
| PUT | Replace | Yes |
| PATCH | Partial update | No |
| DELETE | Remove | Yes |
Three practices
- Plural resource names: use
/usersnot/user; distinguish collection from item with/users/{id}; - Express result with status codes: 201 created, 400 bad input, 401 unauthorized, 404 missing, 500 server error;
- Paginate and filter: lists use
?page=2&size=20so you never return everything at once.
Two anti-patterns
- Verbs as resources: prefer a status field with PATCH over POST
/users/{id}/ban; - Ignoring idempotency: retrying POST can double-create; key critical writes with an idempotency key.
Try it
Validate JSON from an API: JSON formatter.
Real-world cases: three designs that frustrate callers
- Verbs in the path:
POST /getUserListcannot use HTTP semantics or caching; useGET /usersand let the method express the action. - Always returning 200: success and failure both as 200 with a business code in the body leaves gateways and retries blind. Failures belong in 4xx / 5xx.
- Offset-only pagination: with continuous writes, offset paging skips or repeats rows. Provide cursor pagination for fast-changing lists.
FAQ
Must REST use plural nouns? It is a convention, not a rule; the real requirement is using one style consistently per resource. PATCH or PUT? PUT replaces the whole resource, PATCH updates part of it — PATCH fits a single-field change. What belongs in an error response? A stable error code, a readable message and per-field reasons, never internal stack traces. Is an OpenAPI document required? Recommended for public APIs: it underpins the contract, automated tests and SDK generation.
Three contracts to settle up front: idempotency, concurrency, caching
- Idempotency key: write endpoints (orders, payments, job submission) should accept an
Idempotency-Key; deduplicate on it and return the same result within the retention window so a retry cannot double-charge; - Concurrency control: expose
ETagwithIf-Match(optimistic locking) on updates and return412on conflict — far safer than blind overwrite; - Cache semantics: state which responses are cacheable and for how long via
Cache-Control; list endpoints default tono-store, and only static assets get long caching with hashes.
Two shapes of pagination
- Cursor:
?cursor=…&limit=50— stable when new rows keep arriving and no degradation on deep pages; - Offset:
?offset=…&limit=50— simple, but database cost grows with offset and inserts cause skipped or repeated rows; - Either way, return an explicit "has more" flag rather than making clients infer it from a short page.
Details that get overlooked
- Time format: use UTC ISO 8601 with a
Z; never put local-time strings on the wire; - Null versus absent: define the semantics of
nulland of a missing field, and keep them stable; - Large integers: send values above 2^53 (order IDs, say) as strings to avoid JSON precision loss;
- Stable error codes: once published, an error code must keep its meaning — add a new one instead of redefining it.
Health and operational endpoints
- Separate liveness and readiness: liveness asks whether the process should restart, readiness whether it can take traffic — conflating them makes rolling updates briefly take everything down;
- Keep probes cheap: a health check must not trigger full scans or downstream calls, or the check becomes the outage;
- Leak nothing internal: return status and version only, never dependency addresses or environment variables;
- Metadata endpoint: a
/versionroute helps answer "what is running in production" — include the build hash, not just a version number.
The spec is the contract
Keep the API definition (OpenAPI or similar) in version control as the single source of truth, generating types, docs and tests from it. Hand-maintained docs always drift, and stale docs are worse than none — callers build against a wrong understanding.