gitGood.dev
Systems

REST API Design

SystemsFREELast updated: June 2026 · By gitGood Editorial

Method semantics and idempotency, the ~15 status codes that matter, resource naming rules, offset vs cursor pagination, versioning and auth tradeoffs, error body conventions, rate-limit headers, and the smells reviewers flag.

How API design gets graded

"Design the API" appears inside nearly every system design round, and as its own screen for backend roles. The grading is mostly about consistency and failure handling, not REST purity: do your methods match their semantics, do you return the right status codes, can a client retry safely (idempotency), and does the API survive growth (pagination, versioning) without breaking existing clients. Two vocabulary words to wield precisely - safe = no server state change (GET, HEAD); idempotent = N identical requests have the same effect as one (everything except POST and PATCH, by contract).

Method semantics and idempotency

GET
Read a resource. Safe, idempotent, cacheable. Never mutate on GET - crawlers and prefetchers will execute it for you.
HEAD
GET without the body - headers only. Existence checks, size probes before download.
POST
Create a subordinate resource or trigger a process. NOT idempotent by default - the double-submit problem. Make it retry-safe with an Idempotency-Key header (Stripe-style: server stores key -> first response, replays return it). Returns 201 + Location on create.
PUT
Full replace at a client-known URI. Idempotent - PUT the same document twice, same end state. Sending a partial body and calling it PUT is the classic semantics violation; that's PATCH.
PATCH
Partial update. Not guaranteed idempotent (a JSON Patch "append to array" op isn't; a merge-patch of fields usually is). Formats: JSON Merge Patch (RFC 7386, simple) vs JSON Patch (RFC 6902, op-list).
DELETE
Remove the resource. Idempotent in effect - deleting twice leaves the same state; the second call may honestly return 404, which clients should treat as success.
OPTIONS
Capability discovery; what CORS preflight uses. You mostly just need to not break it.

Status codes that matter

Precision here is a strong signal. The pairs interviewers probe: 401 vs 403, 400 vs 422, 502 vs 503 vs 504.

200 OK
Generic success with a body. The default for reads and updates.
201 Created
Resource created - return a Location header pointing at it, and usually the representation.
202 Accepted
Work queued, not done - async processing. Return a status URL the client can poll.
204 No Content
Success, nothing to say. DELETEs and side-effect-only updates.
301 / 308
Permanent redirect. 308 preserves the method (a POST stays a POST); 301 lets clients downgrade to GET.
304 Not Modified
Conditional GET hit: client's ETag (If-None-Match) still current. Free bandwidth via caching.
400 Bad Request
Malformed request - unparseable JSON, missing required field, bad types.
401 Unauthorized
Misnamed: means unauthenticated - no/invalid credentials. "Who are you?"
403 Forbidden
Authenticated but not allowed. "I know who you are - no." (Some APIs return 404 instead, to avoid leaking that the resource exists.)
404 Not Found
No such resource. Also the polite mask for 403 on private resources.
409 Conflict
Request conflicts with current state: duplicate unique key, version conflict, state machine violation (cancelling a shipped order).
412 Precondition Failed
Conditional write lost the race: If-Match ETag is stale. The HTTP face of optimistic concurrency.
422 Unprocessable Entity
Parseable but semantically invalid - email field that isn't an email. Common split: 400 = can't parse, 422 = parsed but invalid. Pick one convention and apply it everywhere.
429 Too Many Requests
Rate limited. Include Retry-After and rate-limit headers (see below).
500 Internal Server Error
Unhandled server fault. Return a correlation/request ID in the body; never a stack trace.
502 / 503 / 504
Upstream spoke garbage (502: bad gateway) / service overloaded or in maintenance, retry later (503, with Retry-After) / upstream timed out (504). Knowing which is which signals production experience.

Resource naming conventions

  • ·Nouns, not verbs: POST /orders, not /createOrder. The method is the verb.
  • ·Plural collections, ID for members: /users and /users/123. Pick plural and never mix.
  • ·Nest only for true ownership, one level deep: /users/123/orders is fine; /users/123/orders/456/items/789/reviews is not. Once a child has its own identity, give it a top-level home: /orders/456.
  • ·Lowercase kebab-case in paths (/credit-notes), snake_case or camelCase in JSON bodies - choose one body convention and enforce it API-wide.
  • ·Filtering, sorting, pagination via query params: GET /orders?status=shipped&sort=-created_at&cursor=abc. Path = identity, query = view.
  • ·Non-CRUD actions: prefer state changes on the resource (POST /orders/123/cancellation, or PATCH status) over RPC-ish verbs. If you must, a scoped action endpoint - POST /orders/123:cancel - beats a global /cancelOrder.
  • ·No file extensions, no trailing-slash ambiguity, and never expose internal DB ids if enumeration is a risk - use UUIDs/slugs.
  • ·Searches with huge criteria: POST /searches creating a search resource sidesteps URL length limits without pretending POST is GET.
  • ·Consistency beats elegance: a mediocre convention applied uniformly is a better API than a clever one applied 80% of the time.

The big three forks

Pagination: offset vs cursor

Offset / limit

?offset=200&limit=50 (or page=5). Trivial to implement, jump-to-page-N works, total counts easy. Degrades at depth (the DB scans and discards offset rows) and skips/duplicates items when rows are inserted or deleted mid-pagination.

Cursor / keyset

?cursor=<opaque token> encoding the last-seen sort key (WHERE (created_at, id) < (...) ORDER BY ... LIMIT 50). Constant cost at any depth, stable under concurrent writes. No random page access, requires a unique total ordering, cursors should be opaque (base64) so clients can't construct them.

When to choose each

Cursor for anything infinite-scroll, high-write, or large (feeds, logs, transaction lists) - it's the default for public APIs at scale (Stripe, Slack, GitHub's newer endpoints). Offset survives where humans need numbered pages over small, slow-changing sets (admin tables, search result pages). The interview sound bite: offset is O(offset) and unstable under writes; keyset is O(limit) and stable.

Versioning

URI path (/v1/users)

Explicit, visible in logs and curl, trivially routable to different backends. Purists object that the resource identity changes; everyone ships it anyway.

Header (custom or Accept media type)

Accept: application/vnd.api+json;version=2 or X-API-Version: 2. Keeps URIs clean and is the "correct" content-negotiation answer; harder to test in a browser, easy for clients to omit, cache keys need Vary.

Query param (?version=2)

Easy to try, ugly to cache and easy to forget. Rarely the primary mechanism.

When to choose each

Default to /v1 in the path - boring, observable, universally understood. The deeper answer interviewers want: versioning is a last resort. Design additively (new optional fields and endpoints are not breaking; clients must tolerate unknown fields), so v2 only happens for true semantic breaks. Stripe's date-pinned account versions are the famous header-based exception.

Auth: API key vs OAuth2 vs JWT

API key

Static bearer secret identifying an app/server. Dead simple. No standard expiry, no delegation, coarse-grained; theft = full access until rotated. Server-to-server only, always over TLS, hash at rest, support rotation.

OAuth2 (+ OIDC)

Delegation framework: user grants a client scoped access without sharing their password; short-lived access tokens + refresh tokens. The standard for third-party access and user-facing login (via OIDC). Cost: real protocol complexity - authorization code + PKCE flow, token endpoints, refresh rotation.

JWT (token format, not a flow)

Self-contained signed claims; any service can verify locally with the public key - stateless, no session-store lookup. Tradeoff: revocation is hard (valid until exp), so keep them short-lived (5-15 min) with refresh tokens, and keep payloads lean. Often the format OAuth2 access tokens take.

When to choose each

Server-to-server internal or simple partner APIs: API keys. Anything where a user authorizes a third-party app: OAuth2 - never invent your own delegation. First-party login at scale: OIDC issuing short-lived JWTs, verified statelessly at each service, with refresh-token rotation handling revocation. The trap question is "how do you revoke a JWT?" - answer: short expiry + refresh rotation, or a denylist check (which quietly reintroduces the state JWTs were avoiding).

Error body conventions

One machine-readable error shape, API-wide. RFC 9457 (Problem Details, application/problem+json) is the standard worth naming; these are its fields plus the two pragmatic extras everyone adds.

type
URI identifying the error category (https://api.example.com/errors/insufficient-funds). Doubles as a documentation link. Stable - clients switch on this, not on prose.
title
Short human-readable summary of the category. Doesn't change per occurrence.
status
The HTTP status code, repeated in the body for logs and proxies.
detail
Human-readable explanation of this occurrence: "Balance is $30, transfer was $50." Never a stack trace, never internal class names.
errors[] (extension)
Field-level validation details for 400/422: [{ "field": "email", "code": "invalid_format", "message": ... }]. Lets a form highlight every bad field in one round trip.
requestId (extension)
Correlation ID echoed from the request or generated. The single highest-value field for support: "give me the requestId" replaces guesswork.
Anti-patterns
200 with { "error": ... } in the body (breaks every HTTP-aware client and cache); different error shapes per endpoint; leaking SQL/stack traces; error prose as the only signal (unparseable by clients).

Rate limiting - the contract with clients

Algorithm choices live in the System Design Tradeoffs sheet; this is the HTTP surface area.

429 + Retry-After
The reject path. Retry-After: 30 (seconds or HTTP date) tells well-behaved clients exactly when to come back. Also legitimate on 503.
X-RateLimit-Limit / -Remaining / -Reset
The de facto trio on every response: quota in the window, what's left, when the window resets (commonly a Unix timestamp - GitHub style). Lets clients self-throttle before hitting 429.
RateLimit-* (IETF draft)
The standardized successors (RateLimit-Limit/-Remaining/-Reset, with delta-seconds reset). Mention-worthy; X- prefixed forms still dominate in the wild.
Scope your keys
Limit per API key / user / IP - and say which in the docs. Tiered limits (free vs paid) and per-endpoint budgets (cheap reads vs expensive searches) are the expected follow-ups.
Client etiquette to design for
Retries with exponential backoff + jitter, honoring Retry-After. If you also own SDKs, build this in - thundering-herd retries after an outage are self-DDoS.

Design smells

  • ·Verbs in paths (/getUser, /createOrder) - the method already says it.
  • ·200 OK wrapping an error payload. Status codes exist; use them.
  • ·POST for everything, including reads. Breaks caching, retries, and semantics in one move.
  • ·Mutations on GET. Prefetchers and crawlers will happily delete your data.
  • ·No pagination on collection endpoints - works in the demo, melts at row one million.
  • ·Unbounded response sizes and no field selection: clients fetch 4KB objects to read one name.
  • ·Chatty resource design: N+1 round trips (/orders then /orders/{id} x 50) because the list endpoint returns only IDs. Lists should return usable summaries; consider ?include= or ?fields= for expansion.
  • ·Breaking changes without a version bump: renaming/removing fields, changing types, tightening validation. Additive-only is the discipline.
  • ·Inconsistent casing/shapes across endpoints (snake_case here, camelCase there; bare arrays here, { data: [...] } envelopes there).
  • ·Timestamps as local time or epoch-sometimes-seconds-sometimes-millis. ISO 8601 UTC ('2026-06-10T14:30:00Z'), everywhere, no exceptions.
  • ·Returning bare top-level JSON arrays - can't add metadata (pagination cursors, counts) later without breaking clients. Envelope collections from day one.
  • ·Idempotency unaddressed on POST payments/orders. The follow-up is always "what happens when the client retries on timeout?"

Other cheat sheets

Big-O Reference

Algorithms

Time and space complexity for the data structures, sorting algorithms, and search routines that show up in coding interviews. Skim the row, remember the row, defend the row in an interview.

Interview Patterns

Patterns

The recurring shapes - sliding window, two pointers, fast/slow, BFS/DFS, backtracking, DP, divide & conquer, binary search variants, union-find, topological sort. Each entry: when to reach for it, the template, complexity, and which classic problems use it.

Design Tradeoffs

Systems

The recurring forks in system design interviews. CAP, PACELC, sync vs async, push vs pull, SQL vs NoSQL, sharding shapes, consistency models, cache strategies, idempotency, and rate limiting. For each, the options and when to choose each.

Unix Essentials

Tools

Filesystem layout, the commands you actually use (find / grep / awk / sed / xargs), processes and signals, networking, permissions, basic shell scripting, and a vi survival kit.

SQL Essentials

Tools

Query clause order, every JOIN type and when to use it, aggregates vs window functions, what indexes actually buy you, transaction isolation levels, and the NULL / WHERE-vs-HAVING / EXISTS-vs-IN gotchas interviewers fish for.

Git Essentials

Tools

The everyday commands, every undo scenario mapped to its fix, rebase vs merge with a side to pick, interactive rebase, bisect, the reflog safety net, stash, and the flags worth aliasing.

Docker & K8s

Tools

The docker and kubectl commands you reach for daily, Dockerfile best practices, how layer caching actually works, the core k8s objects in one screen, requests vs limits, liveness vs readiness, and a step-by-step CrashLoopBackOff debug flow.

STAR Method

Patterns

The STAR structure with timing, what interviewers actually grade, eight question archetypes and how to frame each, the anti-patterns that sink answers (rambling, "we" instead of "I", no metrics), and a 30-second answer skeleton.

Practice the patterns

Reading is the floor. The signal in interviews comes from working problems out loud and defending your tradeoffs. Spin up an AI mock interview or run a coding challenge to put these to work.