← Back to all articles

REST API Design and Resource Modeling

APIBeginner

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

MethodMeaningIdempotent
GETReadYes
POSTCreateNo
PUTReplaceYes
PATCHPartial updateNo
DELETERemoveYes

Three practices

  1. Plural resource names: use /users not /user; distinguish collection from item with /users/{id};
  2. Express result with status codes: 201 created, 400 bad input, 401 unauthorized, 404 missing, 500 server error;
  3. Paginate and filter: lists use ?page=2&size=20 so 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

  1. Verbs in the path: POST /getUserList cannot use HTTP semantics or caching; use GET /users and let the method express the action.
  2. 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.
  3. 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

  1. 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;
  2. Concurrency control: expose ETag with If-Match (optimistic locking) on updates and return 412 on conflict — far safer than blind overwrite;
  3. Cache semantics: state which responses are cacheable and for how long via Cache-Control; list endpoints default to no-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 null and 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 /version route 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.