Cheat sheet · API Design: REST, GraphQL and gRPC From Zero

API Design cheat sheet

How to design an HTTP API that stays boring and safe: names, verbs, shapes, errors, lists, change, retries, security, contracts, and when to pick GraphQL or gRPC. An API is a contract.

Read the full series →

blogs · jp-blogs.vercel.app/cheatsheets/series/api-design

Resources and URLsLesson 0 →

  • Nouns in the URL, verbs from HTTP, numbers from HTTP status codes. POST /getUser is a smell.
  • Plural collections (/orders/42), one case style, nest only for ownership and stop at two levels.
  • Path is identity, query string is options: /orders?status=paid&limit=20.
  • Action that is not CRUD: make a sub-resource, POST /orders/42/cancellation.
  • Return an object ({"items":[...]}), not a bare array, so you can add fields later.
  • Stateless: each request carries all it needs, so any server can answer it.

Watch out: Never delete behind a GET: crawlers and prefetchers will call it. Never answer 200 with {"ok":false}.

Methods: safe and idempotentLesson 1 →

MethodUseSafeIdempotent
GET / HEADReadyesyes
POSTCreate, or run an actionnono
PUTReplace the whole resourcenoyes
PATCHChange a few fieldsnono
DELETERemovenoyes
OPTIONSAsk what is allowedyesyes
  • Idempotent means the same state after many calls, not the same reply: DELETE gives 204, then 404.
  • PUT with a missing field drops that field. Use PATCH for partial change.
  • Creating POST returns 201 plus a Location header.
GET /orders/7
<- 200  ETag: "v3"

PATCH /orders/7
If-Match: "v3"
<- 200 new ETag "v4"

(second editor, still "v3")
<- 412 Precondition Failed

Reload, merge, and try again with the new ETag.

Status codes that matterLesson 1 →

CodeMeaning
200 / 201 / 204OK with body / created / OK with no body
202Accepted, work not done yet (job)
304Not modified, use your cached copy
400Broken request: bad JSON, wrong type
401Not authenticated (who are you?)
403Authenticated, not allowed
404Not found (or hidden on purpose)
409Clashes with current state
412If-Match failed, your version is old
422Valid JSON, values break the rules
429Rate limited, send Retry-After
500 / 502 / 503 / 504Bug / bad upstream / overloaded / upstream slow

Tip: Retry network errors, 429, 503 and 504 with backoff. Do not retry 4xx that fails the same way.

Shapes and formatsLesson 2 →

Dates
RFC 3339 in UTC with Z: 2026-10-11T15:00:00Z
Money
Integer minor units plus currency: {"amount":1999,"currency":"EUR"}
Big ids
Strings, so JavaScript does not round them
Names
One style everywhere (snake_case or camelCase)
null vs absent
Decide once and document it
Envelope
Only for collections (items plus paging)

Watch out: Floats lie: 0.1 + 0.2 = 0.30000000000000004. Never use them for money.

Input, output and errorsLesson 2 →

  • Never return the database row. Build the response with a mapper (an allow-list of fields).
  • Mass assignment: never copy unknown input keys into the database. One extra "role":"admin" made an admin.
  • The edge checks the shape. The database enforces rules that must hold under races (unique, foreign key).
// Content-Type: application/problem+json
{ "type": ".../problems/validation-error",
  "title": "Invalid fields",
  "status": 422,
  "detail": "1 problem found.",
  "instance": "/users",
  "errors": [{"pointer": "/email"}] }

RFC 9457. Never leak stack traces or table names: log them and send a request id.

Pagination, filtering, sortingLesson 3 →

QuestionOffsetCursor
Deep pagesSlower with depthFlat cost
Data changes while pagingDuplicates and gapsCorrect
Go to page 7YesNo
Sync or exportRiskyRight choice
  • Every list gets a default limit, a max limit and a stable order from version one.
  • Cursor query: WHERE (created_at, id) < (?, ?) ORDER BY created_at DESC, id DESC LIMIT n+1.
  • Sort needs a tie-breaker (id). Without it a test lost 3 of 12 rows.
  • Make cursors opaque, tie them to the query, return a next link.
  • Allow-list sort fields. Never paste client text into ORDER BY.
  • Skip total counts by default: expensive and hard to remove later.

Breaking and safe changesLesson 4 →

Change to a responseVerdict
Add an optional field or endpointSafe
Remove, rename or retype a fieldBreaking
Add a value to a returned enumBreaking unless enum is open
Stricter request validationBreaking
New required request fieldBreaking
Add pagination to an unpaged listBreaking
Change a status or error codeBreaking
  • Expand and contract: add the new, keep the old, migrate clients, remove the old.
  • Clients are tolerant readers: ignore unknown fields, handle unknown enum values.

Versioning and deprecationLesson 4 →

StyleTrade-off
/v1/ in pathObvious and easy to route, new URLs for all
?api-version=dateStable paths, easy to forget
HeaderClean URLs, hidden, needs Vary
Media typePer resource, weaker tooling
  1. Mark deprecated: true in the spec and write a migration guide.
  2. Send Deprecation (RFC 9745) and Sunset (RFC 8594) headers with the date.
  3. Monitor who still calls it and get client consent.
  4. After the sunset date answer 410 Gone with a problem body.

Watch out: Never move unversioned callers to the newest version silently. Keep one internal model, one translator per version.

Idempotency keys and retriesLesson 5 →

POST /payments
Idempotency-Key: 8e03978e-40d5-...

{"amount": 500}

# same key, same body -> replay reply
# still running        -> 409
# same key, new body   -> 422

One key per intended action, not per attempt. Scope it by account, keep it a day or more.

  • Store key, owner, body fingerprint, state, status and response.
  • Claim the key atomically in the database. A check-then-save gap let three charges through.
  • Client backoff: exponential delay plus jitter, honour Retry-After, cap the tries.
  • Slow work: reply 202 plus Location of a job resource, then poll or send a webhook.

Webhooks and rate limitsLesson 5 →

sig = "v1," + base64(
  HMAC_SHA256(secret,
    `${id}.${ts}.${rawBody}`))
// headers: webhook-id,
//  -timestamp, -signature

Sign the raw bytes. Compare in constant time (timingSafeEqual). Reject old timestamps (about 5 min).

  • Duplicates are normal: dedupe by event id. Do not promise order. Reply fast, process later.
  • Answer limits with 429, Retry-After and a problem body. Count by API key or user, not just IP.
LimiterNote
Fixed windowSimple, but allows 2x burst at the edge
Sliding windowSmooth, more memory
Token bucketBursts up to B, steady rate R
Concurrency limitGuards slow endpoints

Auth: keys, JWT, OAuthLesson 6 →

  • 401 not authenticated, 403 not allowed, 404 to hide that an object exists.
  • API keys identify apps: store only hashes, show once, add a prefix, scopes and rotation. Never in URLs or in mobile code.
  • A JWT is signed, not secret. Set algorithms yourself, check iss, aud, exp, reject alg: none.
  • Access tokens 5 to 15 minutes; refresh tokens go only to the auth server. Prefer RS256 over a shared HS256 secret.
SituationFlow
User in web, mobile or SPAAuthorization code + PKCE
Server as itselfClient credentials
TV or CLIDevice authorization
Implicit, passwordDo not use
  1. Client makes a code_verifier; challenge = base64url(SHA-256(verifier)).
  2. Browser goes to /authorize with state, code_challenge, S256.
  3. Redirect returns a code; client checks state.
  4. Client POST /token with code and verifier; server checks the hash.

Authorization: where APIs breakLesson 6 →

OWASP APIMeaning
API1 BOLAObject id taken, owner never checked
API3 Property levelMass assignment and over-exposure
API5 Function levelAdmin function open to users
API7 SSRFAPI fetches a caller-given URL

Tip: Scopes limit the app. Ownership checks limit the user. You need both. Take the user from the token, never ?userId=. Default to deny.

OpenAPI contractLesson 7 →

openapi / info
Spec version (3.1.0), title, your API version
servers / paths
Base URLs / every URL and its methods
components
Reusable schemas, responses, parameters; use $ref
security
Which scheme guards which operation
webhooks
Calls your server sends out (3.1)
  • Use a validator and a linter: a weak file was valid, yet lint found an undeclared path parameter.
  • One file gives docs, request checks, mock server, SDKs and breaking-change diffs.
  • Contract test against the real server catches drift (right status, four wrong fields).
  • Requests: additionalProperties: false. Responses: stay open.
  • Spec-first for shared APIs, code-first for one team. It cannot state business rules.

GraphQL and gRPCLesson 8 →

AspectRESTGraphQLgRPC
Best forPublic CRUDVaried clientsInternal calls
WireJSONJSONProtobuf
CachingHTTP, easyHardIn the app
BrowserNativeNativeNeeds gateway
RiskRound tripsN+1, costTooling
  • GraphQL returns 200 even with errors: always read the errors array. ! decides how far a null spreads.
  • N+1: DataLoader, one per request (11 DB calls became 2).
  • Client sets the cost: limit depth, list size, cost, timeouts, batching. Depth 9 meant 9,841 calls.
  • gRPC: field numbers are the contract. Use deadlines, not plain timeouts.
INVALID_ARGUMENT
400
NOT_FOUND / EXISTS
404 / 409
UNAUTHENTICATED / DENIED
401 / 403
RESOURCE_EXHAUSTED
429
UNAVAILABLE / DEADLINE
503 (retry) / 504

API review checklistLesson 9 →

  1. Names and URLs, then methods and status codes.
  2. Shapes and one error format, then lists.
  3. Change and compatibility, then retries.
  4. Security (BOLA first), then docs and contract.
  5. Style fit (REST, GraphQL, gRPC) and operations.
  • Common smells: verbs in URLs, always 200, float prices, client sends price, X-User-Id as auth, unbounded lists.
  • Protect a business rule at the lowest layer: a database constraint stops double booking.

Interview one-linersLesson 9 →

Safe vs idempotent
Safe changes nothing; idempotent gives the same state when repeated
POST times out
Retry with the same Idempotency-Key
401 vs 403
Who are you vs you may not
Offset vs cursor
Offset slows and skips; cursor stays flat
Breaking change
Removing, renaming, retyping, tighter input
Verify a webhook
HMAC of raw body, timestamp, constant time
BOLA
Missing owner check on an object id
Charged twice?
Check for key reuse and a non-atomic claim
Choose a style
REST first, GraphQL for varied clients, gRPC inside

A cheat sheet is a reminder, not a teacher. Each card links to the lesson that explains it step by step.