Library
00/07 · ~36 min
GUIDEDECK · designing interfaces other people build on

API
Design & the contracts
that outlive your code.

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.

~36 MINBEGINNER → INTERMEDIATEHTTP / WEB APIs
SCROLL
01 · What makes an API good 4 min

An API is a promise
you have to keep for years.

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.

APIApplication Programming Interface — is the contract one piece of software exposes so others can use it without knowing how it works inside. In this deck we mean web APIs: a server publishes endpoints over HTTP, and clients — browsers, mobile apps, other services — call them. The transport underneath (TCP, TLS, HTTP) lives in Networking.

Three qualities that matter

  • Consistency — the same idea looks the same everywhere. Once a caller learns one endpoint, the next one should feel familiar without reading the docs.
  • Predictability— names, errors, and status codes mean what a reasonable person expects. No surprises, no "200 OK" wrapping an error.
  • Evolvability— you can add capability without breaking anyone who already integrated. The contract grows; it doesn't shatter.
client app · service API contract URLs · fields server DB · logic (free to change) depends on implements change internals freely · keep the contract stable

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.

Least surprise

The best API is the one a developer guesses correctly on the first try.

Hard to misuse

Make the right call obvious and the dangerous call awkward or impossible.

Docs are a tax

Every inconsistency is a sentence someone has to write — and another reader has to look up.

02 · Resource modeling & REST conventions 6 min

Model nouns as URLs,
let verbs be the HTTP methods.

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.

RESTRepresentational State Transfer — is a style for HTTP APIs where you expose resources(a user, an order, a comment) at clean URLs and act on them with the standard HTTP verbs. It's a set of conventions, not a strict spec — which is why "RESTful" covers a wide range of discipline. The verbs and status codes themselves get the deep treatment in HTTP Request Methods.
/orders collection /orders/42 one item /orders/42/items sub-collection GET list POST create GET read PATCH update DELETE remove

Plural nouns, nested by ownership. The same five verbs work on every level — no new vocabulary per resource.

The conventions worth following

  • Plural nouns for collections: /orders, not /getOrder. The verb is already in the method.
  • Nest by ownership, shallowly: /orders/42/items is fine; five levels deep is a smell.
  • Hyphenate, lowercase: /shipping-addresses, never /ShippingAddresses.
  • Return the right status— 201 on create, 204 on a body-less delete, 404 when it's really gone.
RPC-flavored — a verb per action
// every action invents a new endpoint + verb POST /createOrder POST /getOrderById POST /updateOrderStatus POST /deleteOrder?id=42 POST /addItemToOrder // 200 OK { "error": "not found" } ← lies to the client
Resource-oriented — nouns + methods
// one noun, the method carries the intent POST /orders // 201 Created + Location GET /orders/42 // 200, or 404 if absent PATCH /orders/42 // 200, partial update DELETE /orders/42 // 204 No Content POST /orders/42/items // add a child
PUT vs PATCH

PUT replaces the whole resource; PATCHchanges part of it. Sending only the fields that changed? That's PATCH.

Actions that aren't CRUD

"Publish" or "refund" don't map to a verb. Model them as a sub-resource — POST /orders/42/refunds — not /refundOrder.

Status, honestly

2xx success, 4xxthe caller's fault, 5xx yours. Never bury an error inside a 200.

03 · REST vs GraphQL vs gRPC 6 min

Three protocols, three
honest trade-offs.

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.

The one-line difference REST gives you many resource URLs and the client takes what it's given; GraphQL gives you one endpoint and the client picks exactly the fields it wants; gRPC gives you typed function calls over a fast binary protocol. They can coexist: REST at the public edge, gRPC between internal services, GraphQL in a BFF for rich frontends.
REST · over-fetch + round-trips client GET /user → big GET /orders GET /prefs GraphQL · pick fields, one trip client { user { name, orders { total } } } → exactly those fields

REST can over-fetch and need several calls for one screen; GraphQL folds that into a single shaped request.

What each is really for

  • REST — the universal default. Cache-friendly, debuggable with curl, understood by everyone. The right answer for most public APIs.
  • GraphQL — when many different clients want many different slices of the same graph, and over-fetching is real pain. You trade that for server-side complexity.
  • gRPC — internal, high-throughput, low-latency service calls where a strict schema and code generation pay off. Not browser-native.

Tooling landscape — the three styles, honestly

GET /v1/orders/42 HTTP/1.1 Accept: application/json // → 200 OK { "id": "42", "total": 90, "status": "paid" }

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.

# one endpoint, the client shapes the result query { order(id: "42") { total items { name } # only what the UI needs } }

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.

// schema-first; codegen builds client + server service Orders { rpc Get(OrderId) returns (Order); } message Order { string id = 1; int32 total = 2; }

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.

04 · Pagination, filtering & error shapes 5 min

Never return
an unbounded list.

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.

Pagination returning a large collection in bounded pages instead of all at once. Two common styles: offset (skip N, take M — simple but drifts and slows down deep in the list) and cursor (an opaque token pointing at a stable position — scales and survives inserts). Cursor wins for large or fast-changing data.
OFFSET · ?offset=20&limit=10 skip 20 an insert shifts rows → dup/skip slow at deep offsets CURSOR · ?after=ord_88 anchor: ord_88 stable under inserts · indexed seek next = id of last row returned

Offset can skip or duplicate rows when the list changes mid-scroll; a cursor anchors to a real row and seeks straight to it.

Filtering & sorting that stay sane

  • Filters as query params: ?status=paid&created_after=2026-01-01. Keep names consistent with your field names.
  • One sort param: ?sort=-created_at (a leading - for descending) beats five bespoke flags.
  • Return paging metadata — the next cursor and whether more exists — so the client never has to guess.
  • Always cap limit server-side. A client asking for 1,000,000 should quietly get your max.
// cursor-paged response — data + how to continue { "data": [ { "id": "ord_88" }, /* … */ ], "page": { "next": "ord_98", // pass as ?after= "has_more": true } }
// RFC 9457 problem details — one shape for every error // Content-Type: application/problem+json (HTTP 422) { "type": "https://api.acme.com/errors/validation", "title": "Invalid request", "status": 422, "detail": "total must be a positive integer", "errors": [ { "field": "total", "code": "min" } ] }

One predictable error envelope across the whole API beats a different shape per endpoint. RFC 9457 is the standard form.

Machine-readable code

Include a stable code string clients can branch on. Human detail text will change; the code must not.

Right status + body

400 malformed, 401/403 auth, 404 missing, 409 conflict, 422 validation. The status and the body should agree.

Don't leak internals

No stack traces or SQL in error bodies. Log the gory detail server-side; return a tidy, safe shape to the caller.

05 · Versioning & backwards compatibility 5 min

Add freely. Remove and
rename only with a new version.

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.

Backwards compatible a change is safe if every client written against the old contract keeps working unchanged. Additive changes — new optional fields, new endpoints, new optional params — are safe. Breaking changes — removing or renaming a field, tightening a type, making something required — force every caller to update, so they need a new version.
SAFE · add optional field old client ignores new field ✓ + "currency" added, optional BREAKS · rename / remove field old client reads "total" → null ✕ total → amount renamed

Adding an optional field harms no one. Renaming or removing one silently breaks every client still reading the old name.

Where to put the version

  • URL path/v1/orders. Blunt but obvious, trivially cacheable and routable. The most common choice.
  • HeaderAccept: application/vnd.acme.v2+json or a date-based version header. Keeps URLs clean; harder to see and test.
  • No version, evolve additively — viable if you stay strictly backwards compatible. The best version is the one you never have to ship.
Breaking, silently
// same /v1 endpoint, payload changed underneath // before { "name": "Dani Ortiz" } // after — split into two, dropped "name" { "first": "Dani", "last": "Ortiz" } // every client reading .name now gets undefined ✕
Additive, then deprecate
// keep the old field, add the new ones { "name": "Dani Ortiz", // kept, marked deprecated "first": "Dani", "last": "Ortiz" } // announce a sunset date → remove in /v2 later
Deprecate, don't delete

Mark a field deprecated, send a Deprecation / Sunset header, give clients a real window before it disappears.

Tolerant reader

Clients should ignore unknown fields rather than choke on them — that's what makes additive changes safe in the first place.

Few versions, long-lived

/v1/v2 is fine; a new version every quarter is a support nightmare. Make versions rare and durable.

06 · Idempotency, rate limiting & auth 6 min

Make retries safe, abuse
bounded, and access proven.

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.

Idempotent making the same call twice has the same effect as making it once. 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.
client retries server key → result store charge created once POST key=k1 (1st) POST key=k1 (retry) only 1st

The retry carries the same Idempotency-Key; the server recognizes it and replays the stored result — one charge, not two.

# client generates a unique key per logical request POST /v1/charges Idempotency-Key: 8f3c1a90-... # same on every retry { "amount": 9000, "currency": "usd" } # 1st call → creates + stores result under the key # retry → returns the SAME response, no new charge

Popularized by Stripe; an Idempotency-Key header is now a common convention for safe-to-retry writes.

Rate limiting — protect the shared resource

  • Cap requests per client per window (a token bucket is the usual mechanism — refill at a steady rate, spend per call).
  • Over the limit → 429 Too Many Requests with a Retry-After header so the client backs off politely.
  • Advertise budget with RateLimit-* response headers (limit, remaining, reset) so good clients self-throttle.

Auth — authN vs authZ

  • Authentication = who you are; authorization = what you may do. Different questions, often confused.
  • API keys for server-to-server simplicity; OAuth 2.0 / OpenID Connect when users delegate access to third parties.
  • Carry credentials in Authorization: Bearer … over TLS — never in the URL, where they land in logs and history.
# the server tells the client its budget HTTP/1.1 429 Too Many Requests Retry-After: 30 RateLimit-Limit: 100 RateLimit-Remaining: 0 RateLimit-Reset: 30 # seconds until refill
bucket refill: steady rate request → 200 spend 1 token empty → 429

Token bucket: requests spend tokens, the bucket refills steadily, and an empty bucket returns 429 with a back-off hint.

07 · Contract-first & OpenAPI + recap 4 min

Write the contract first.
Generate the rest.

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.

Contract-first / OpenAPI you describe the API in a formal spec (OpenAPI for REST, Protobuf for gRPC, the SDL for GraphQL) and treat that file as the source of truth. From one OpenAPI document you can auto-generate interactive docs, typed clients in many languages, server stubs, mock servers, and runtime request validation — so the spec and the code never drift apart.
# the contract — the source of truth, not an afterthought paths: /orders/{id}: get: parameters: [{ name: id, in: path, required: true }] responses: "200": { $ref: "#/components/schemas/Order" } "404": { $ref: "#/components/responses/Problem" }
OpenAPI one spec docs typed clients mock server validation

One spec, many artifacts. Generate docs, SDKs, mocks, and validation instead of hand-maintaining each.

Spec & tooling landscape

OpenAPI + Swagger / Redoc

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.

Postman / Insomnia

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.

Protobuf + gRPC

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 rules to walk out with

1Design the contract, not the code. The URLs, fields, and status codes are the promise — keep them consistent and predictable.
2Model nouns, use the verbs. Resources at clean URLs, the HTTP method carries the intent, honest status codes throughout.
3Start with REST. Reach for GraphQL or gRPC only when a real pain — over-fetching, internal latency — earns the complexity.
4Evolve, don't break. Add optional fields freely; remove and rename only behind a new version with a sunset window.
5Make it safe to depend on. Idempotent writes, bounded rate limits, real auth, and one consistent error shape.
  • Sketch the resources first. Nouns and their ownership tree before any endpoint.
  • Write the OpenAPI spec and review it with a consumer before writing handlers.
  • Pick the right status + one error shape (RFC 9457) and use it everywhere.
  • Paginate every list, cap every limit, and decide cursor vs offset up front.
  • Plan for change: tolerant readers, additive growth, a versioning rule you'll actually keep.
Knowledge check

Did it stick?

Five quick questions on REST conventions, the protocol choice, pagination, versioning, and idempotency — instant feedback, no sign-in.

Rate this deck
be the first

Navigate with ← → or scroll · back to library