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.
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 /getUseris 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 →
| Method | Use | Safe | Idempotent |
|---|---|---|---|
| GET / HEAD | Read | yes | yes |
| POST | Create, or run an action | no | no |
| PUT | Replace the whole resource | no | yes |
| PATCH | Change a few fields | no | no |
| DELETE | Remove | no | yes |
| OPTIONS | Ask what is allowed | yes | yes |
- Idempotent means the same state after many calls, not the same reply:
DELETEgives204, then404. PUTwith a missing field drops that field. UsePATCHfor partial change.- Creating
POSTreturns201plus aLocationheader.
GET /orders/7
<- 200 ETag: "v3"
PATCH /orders/7
If-Match: "v3"
<- 200 new ETag "v4"
(second editor, still "v3")
<- 412 Precondition FailedReload, merge, and try again with the new ETag.
Status codes that matterLesson 1 →
| Code | Meaning |
|---|---|
| 200 / 201 / 204 | OK with body / created / OK with no body |
| 202 | Accepted, work not done yet (job) |
| 304 | Not modified, use your cached copy |
| 400 | Broken request: bad JSON, wrong type |
| 401 | Not authenticated (who are you?) |
| 403 | Authenticated, not allowed |
| 404 | Not found (or hidden on purpose) |
| 409 | Clashes with current state |
| 412 | If-Match failed, your version is old |
| 422 | Valid JSON, values break the rules |
| 429 | Rate limited, send Retry-After |
| 500 / 502 / 503 / 504 | Bug / 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_caseorcamelCase) - null vs absent
- Decide once and document it
- Envelope
- Only for collections (
itemsplus 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 →
| Question | Offset | Cursor |
|---|---|---|
| Deep pages | Slower with depth | Flat cost |
| Data changes while paging | Duplicates and gaps | Correct |
| Go to page 7 | Yes | No |
| Sync or export | Risky | Right 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
nextlink. - 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 response | Verdict |
|---|---|
| Add an optional field or endpoint | Safe |
| Remove, rename or retype a field | Breaking |
| Add a value to a returned enum | Breaking unless enum is open |
| Stricter request validation | Breaking |
| New required request field | Breaking |
| Add pagination to an unpaged list | Breaking |
| Change a status or error code | Breaking |
- 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 →
| Style | Trade-off |
|---|---|
/v1/ in path | Obvious and easy to route, new URLs for all |
?api-version=date | Stable paths, easy to forget |
| Header | Clean URLs, hidden, needs Vary |
| Media type | Per resource, weaker tooling |
- Mark
deprecated: truein the spec and write a migration guide. - Send
Deprecation(RFC 9745) andSunset(RFC 8594) headers with the date. - Monitor who still calls it and get client consent.
- After the sunset date answer
410 Gonewith 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 -> 422One 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
202plusLocationof 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, -signatureSign 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-Afterand a problem body. Count by API key or user, not just IP.
| Limiter | Note |
|---|---|
| Fixed window | Simple, but allows 2x burst at the edge |
| Sliding window | Smooth, more memory |
| Token bucket | Bursts up to B, steady rate R |
| Concurrency limit | Guards 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
algorithmsyourself, checkiss,aud,exp, rejectalg: none. - Access tokens 5 to 15 minutes; refresh tokens go only to the auth server. Prefer RS256 over a shared HS256 secret.
| Situation | Flow |
|---|---|
| User in web, mobile or SPA | Authorization code + PKCE |
| Server as itself | Client credentials |
| TV or CLI | Device authorization |
| Implicit, password | Do not use |
- Client makes a
code_verifier; challenge = base64url(SHA-256(verifier)). - Browser goes to
/authorizewithstate,code_challenge,S256. - Redirect returns a
code; client checksstate. - Client
POST /tokenwith code and verifier; server checks the hash.
Authorization: where APIs breakLesson 6 →
| OWASP API | Meaning |
|---|---|
| API1 BOLA | Object id taken, owner never checked |
| API3 Property level | Mass assignment and over-exposure |
| API5 Function level | Admin function open to users |
| API7 SSRF | API 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 →
| Aspect | REST | GraphQL | gRPC |
|---|---|---|---|
| Best for | Public CRUD | Varied clients | Internal calls |
| Wire | JSON | JSON | Protobuf |
| Caching | HTTP, easy | Hard | In the app |
| Browser | Native | Native | Needs gateway |
| Risk | Round trips | N+1, cost | Tooling |
- GraphQL returns
200even witherrors: always read theerrorsarray.!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 →
- Names and URLs, then methods and status codes.
- Shapes and one error format, then lists.
- Change and compatibility, then retries.
- Security (BOLA first), then docs and contract.
- Style fit (REST, GraphQL, gRPC) and operations.
- Common smells: verbs in URLs, always
200, float prices, client sendsprice,X-User-Idas 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.