A 36-minute working session on shaping HTTP APIs people actually enjoy using — resource modeling, the REST/GraphQL/gRPC choice, pagination and errors, versioning, idempotency and auth, and writing the contract first. We'll be honest about where the boring option wins.
The moment someone writes code against your endpoint, you've signed a contract. You can rewrite the internals freely, but the shape on the wire — the URLs, the fields, the status codes — is now load-bearing for everyone else. Good API design is the discipline of making that promise easy to read, hard to misuse, and safe to grow.
The client depends on the contract, not your code. Keep the contract stable and you can refactor everything behind it.
clients you'll never meet may depend on a field you thought was private.
The best API is the one a developer guesses correctly on the first try.
Make the right call obvious and the dangerous call awkward or impossible.
Every inconsistency is a sentence someone has to write — and another reader has to look up.
The core REST trick is small but powerful: your URLs name things(resources), and the HTTP method says what you're doing to that thing. GET /orders/42 reads order 42; DELETE /orders/42 removes it. You stop inventing a new endpoint for every action and lean on a handful of methods everyone already knows.
Plural nouns, nested by ownership. The same five verbs work on every level — no new vocabulary per resource.
/orders, not /getOrder. The verb is already in the method./orders/42/items is fine; five levels deep is a smell./shipping-addresses, never /ShippingAddresses.PUT replaces the whole resource; PATCHchanges part of it. Sending only the fields that changed? That's PATCH.
"Publish" or "refund" don't map to a verb. Model them as a sub-resource — POST /orders/42/refunds — not /refundOrder.
2xx success, 4xxthe caller's fault, 5xx yours. Never bury an error inside a 200.
REST, GraphQL, and gRPC aren't a ranking — they're answers to different questions about who calls you and how. Most teams should start with REST and reach for the others only when a concrete pain shows up: REST's over-fetching, or the latency budget of an internal mesh.
REST can over-fetch and need several calls for one screen; GraphQL folds that into a single shaped request.
curl, understood by everyone. The right answer for most public APIs.Pro — universal, cacheable at the HTTP layer, trivially debuggable, huge ecosystem.
Con — over/under-fetching; one screen can mean several round-trips; no built-in schema.
Choose for public APIs, broad reach, and anything you want cached or curl-able.
Pro — client picks exact fields; one round-trip for varied UIs; a strong typed schema.
Con — caching, rate-limiting, and N+1 queries become your problem; server complexity climbs.
Choose for rich, client-driven frontends and BFFs aggregating many sources.
Pro — fast HTTP/2 binary, strict protobuf schema, streaming, generated clients in many languages.
Con — not browser-native (needs gRPC-Web); hard to eyeball; heavier tooling.
Choose for internal service-to-service traffic with tight latency budgets.
Like a menu vs. a build-your-own bowl vs. a standing order: REST hands you set dishes, GraphQL lets each diner compose their plate, gRPC is the pre-agreed contract between the kitchen and its suppliers. When in doubt, start with REST — you can add the others where they earn their keep.
The first time a collection has ten thousand rows, an endpoint that returns "all of them" falls over. Pagination, filtering, and a consistent error shape are the unglamorous details that decide whether your API survives real data — so design them up front, not after the incident.
Offset can skip or duplicate rows when the list changes mid-scroll; a cursor anchors to a real row and seeks straight to it.
?status=paid&created_after=2026-01-01. Keep names consistent with your field names.?sort=-created_at (a leading - for descending) beats five bespoke flags.limit server-side. A client asking for 1,000,000 should quietly get your max.One predictable error envelope across the whole API beats a different shape per endpoint. RFC 9457 is the standard form.
Include a stable code string clients can branch on. Human detail text will change; the code must not.
400 malformed, 401/403 auth, 404 missing, 409 conflict, 422 validation. The status and the body should agree.
No stack traces or SQL in error bodies. Log the gory detail server-side; return a tidy, safe shape to the caller.
Every integrated client is frozen against the contract they wrote to. The cheapest versioning strategy is to almost never need a new version— by changing your API in ways that can't break existing callers. When you genuinely must break something, do it visibly and on a schedule.
Adding an optional field harms no one. Renaming or removing one silently breaks every client still reading the old name.
/v1/orders. Blunt but obvious, trivially cacheable and routable. The most common choice.Accept: application/vnd.acme.v2+json or a date-based version header. Keeps URLs clean; harder to see and test.Mark a field deprecated, send a Deprecation / Sunset header, give clients a real window before it disappears.
Clients should ignore unknown fields rather than choke on them — that's what makes additive changes safe in the first place.
/v1 → /v2 is fine; a new version every quarter is a support nightmare. Make versions rare and durable.
Real networks drop responses, clients retry, and not everyone calling you is friendly. Three mechanisms keep a public API trustworthy: idempotency so a retried write doesn't double-charge, rate limiting so one caller can't starve the rest, and authenticationso you know who's on the line.
GET, PUT, and DELETE are idempotent by definition; POST is not — so for a POST that creates a charge, the client sends an idempotency key and the server remembers the result, returning the original instead of charging twice.The retry carries the same Idempotency-Key; the server recognizes it and replays the stored result — one charge, not two.
Popularized by Stripe; an Idempotency-Key header is now a common convention for safe-to-retry writes.
429 Too Many Requests with a Retry-After header so the client backs off politely.RateLimit-* response headers (limit, remaining, reset) so good clients self-throttle.Authorization: Bearer … over TLS — never in the URL, where they land in logs and history.Token bucket: requests spend tokens, the bucket refills steadily, and an empty bucket returns 429 with a back-off hint.
The most reliable APIs are designed as a written contract before a line of handler code exists. A machine-readable spec becomes the single source of truth — docs, client SDKs, mock servers, and request validation all flow from it, and everyone agrees on the shape before anyone builds it.
One spec, many artifacts. Generate docs, SDKs, mocks, and validation instead of hand-maintaining each.
Pro — the REST standard; one spec drives docs, codegen, mocks, and validation.
Con — large YAML files get unwieldy; keeping spec and code in sync needs discipline or generation.
Choose as the source of truth for any REST API you expect others to consume.
Pro — fast manual exploration, shareable collections, automated test runs against live endpoints.
Con— a collection isn't a contract; it can drift from the real API unless tied to the spec.
Choose for trying, debugging, and integration-testing an API by hand.
Pro — strict schema-first contract with codegen for many languages and built-in compatibility rules.
Con — gRPC-only; not browser-native; binary payloads are hard to inspect by eye.
Choose when the contract is between internal services on gRPC, not public HTTP.
Five quick questions on REST conventions, the protocol choice, pagination, versioning, and idempotency — instant feedback, no sign-in.
Navigate with ← → or scroll · back to library