LearnThatStack Ace your next interview

API Design

REST over HTTP, and gRPC with Go examples Checked Sep 2026 64 code snippets

The model

  • An API is a contract that clients you don't control depend on. Change it by adding, never by removing or renaming.
  • REST names resources with nouns, like /orders/7, and acts on them with HTTP methods.
  • Stateless means each request carries its own token and context. Any server can answer it, so you scale by adding servers.
  • Networks fail and clients retry. Make writes idempotent, so a repeat has the same effect as one call.
  • gRPC keeps the contract in a .proto file and sends binary messages over HTTP/2. It suits calls between your own services.
Clientbrowser or appAPI gatewayedge checksServiceyour codeDatabasethe dataINGET /orders/7 + tokenforward with caller idquery only their rowsREFUSE401 or 429403, 404 or 422REPLY200 + ETag
The gateway refuses a bad token or too many calls. Your code refuses a caller who isn't allowed, a missing record or bad input.

Resources & methods

name things, then act on them with HTTP

Name resources

Name a collectionGET /usersPlural nouns, no verbs.
Pick one item by path, or filter by queryGET /users/123GET /users?role=admin
Show a parent and its childrenGET /users/123/orders
Tell a URI, URL and URN apartURLs locate, URNs name. Both are URIs.

TipKeep nesting shallow. Reach a deep child directly, like /reviews/789.

Pick the HTTP method

Read a resourceGET /users/123Safe and idempotent.
Create one, and let the server pick the idPOST /usersNeither safe nor idempotent.
Create or replace it at an id you choosePUT /users/123Idempotent.
Change only some fieldsPATCH /users/123Not guaranteed to be idempotent.
Remove itDELETE /users/123Idempotent. 204 on success.

TipHEAD and OPTIONS are safe too. A repeat DELETE can return 404 and still be idempotent.

TrapA PUT body is the whole resource. Leave a field out and the server should reset it to its default or null.

Model an action that isn't CRUD

Cancel when only the status changesPATCH /orders/7With {"status": "cancelled"}.
Cancel with a reason or a refundPOST /orders/7/cancellation
Use a custom method, Google stylePOST /orders/7:cancel

TipIf it already shipped, refuse with 409 Conflict and say why.

TrapAvoid bare verb paths like /cancelOrder. They hide the resource the action changes.

Formats and content types

Say what format a body is inContent-Type: application/json
Ask for JSON, with XML as second choiceAccept: application/json, application/xml;q=0.9
Say why most APIs use JSONIt's light and readable, and every language parses it.

TrapSet Content-Type on every body. A browser that guesses the type can read data as something else.

Errors & retries

fail clearly, and retry without harm

Status codes to know

Answer a POST that created something201 Created
Reject a body that won't parse400 Bad Request
Reject a body that fails validation422 Unprocessable Content
Reject missing or bad credentials401 Unauthorized
Refuse a known caller who lacks access403 Forbidden
Turn away a client over its rate limit429 Too Many Requests

Trap401 Unauthorized really means unauthenticated. Send 403 when you know the caller and still refuse.

Shape an error response

Use the standard error formatContent-Type: application/problem+jsonRFC 9457. It replaced RFC 7807.
Name its five standard fieldstype, title, status, detail and instance.
Report every bad field at onceList each with its field name, a stable code and a message.

TipSet the real HTTP status on the response too. Keep stack traces, SQL and hostnames out.

TrapClients must branch on a stable code or type, never on the message text.

Retry without doing harm

Pick which failures to retryTimeouts, dropped connections, 429 and 503.
Space out the retriesExponential backoff with full jitter, so clients don't all retry at once.
Retry a payment without charging twiceIdempotency-Key: <uuid>Repeats get the first result.
Stop a PUT overwriting a newer editIf-Match: "abc123"Fails if the ETag changed.

TipKeep a retry budget. Once retries pass about 10% of requests, fail fast instead.

TrapA write that timed out may already have run. Retry it only if it's idempotent or carries a key.

Auth & security

prove who's calling, then limit what they can do

Authenticate callers

Identify a calling appX-API-Key: <key>Names the app, not the user.
Store an API keyShow the user the key once, then keep only a hash and a prefix.
Check callers with no session storeAuthorization: Bearer <jwt>Hard to revoke before it expires.
Let an app act for a userOAuth 2.0, with scoped tokens you can revoke.
Log a user in from a mobile app or SPACode flow with PKCE. It binds the code to the app that asked.

TrapSend keys in a header, never in the query string. URLs end up in logs.

CORS and security headers

Let one site's pages call your APISend Access-Control-Allow-Origin with that origin.
Know when a preflight OPTIONS comes firstFor a PUT, or any request that isn't simple.
Force HTTPSStrict-Transport-Security: max-age=31536000
Block clickjackingContent-Security-Policy: frame-ancestors 'none'Plus legacy X-Frame-Options.

TipLeave out X-XSS-Protection, or send 0. OWASP says the header can create XSS holes.

TrapCORS only guides browsers. Other clients ignore it, so it never replaces authentication.

Rate limiting

Tell a limited client when to come backRetry-After: 30
Allow short burstsToken bucket. It refills steadily up to its size.
Count requests per clock minuteFixed window. Bursts can double at the edges.

TipA sliding window smooths those edges. Keep every counter in a shared store like Redis.

Paging, caching & speed

serve big reads fast

Page, filter and sort a list

Page by offsetGET /users?offset=40Any page, but deep ones are slow.
Page by cursorGET /users?cursor=abc123Same cost every page. No jumping.
Match any of several valuesGET /orders?status=open,paidCommas mean OR. Separate fields mean AND.
Sort newest firstGET /orders?sort=-created_at,id

TipA leading - sorts descending. End every sort with a unique field like id, or pages repeat and skip rows.

TrapOffset pages shift when rows are inserted between requests, so items repeat or go missing.

Cache responses

Let any cache keep it for an hourCache-Control: public, max-age=3600
Keep per-user data out of shared cachesCache-Control: private
Check a cached copy is still currentIf-None-Match: "abc123"No change gets 304.
Cache gRPC responsesIn a server interceptor keyed by method and request.

TipCaches sit in the browser, a CDN, a reverse proxy, an app cache like Redis and the database.

Make responses faster

Send only the fields asked forGET /users?fields=id,name
Serve phones on slow networksMake fewer, smaller calls, or add a backend for frontend.
Scale by adding serversStateless servers, a cache, pooled connections, then shards.

TipOld app versions stay installed for years, so give each mobile API version a long support window.

Versions & change

change the contract without breaking clients

Version an API

Put the version in the pathGET /v1/usersClear, but it clutters URLs.
Put the version in a headerAPI-Version: v1Clean URLs, but less visible.
Version a gRPC servicepackage user.v2;A new package for breaking changes.

TrapA header or media-type version needs Vary on the response, or shared caches mix up versions.

Change without breaking clients

Add an optional field or endpointSafe. Clients ignore what they don't use.
Remove, rename or retype a fieldBreaking. Ship a new version and run both.
Mark an endpoint deprecatedDeprecation: @1788220800RFC 9745. @ plus Unix seconds.
Say when it stops answeringSunset: Wed, 31 Mar 2027 23:59:59 GMT
Point to the replacementLink: </v2/users>; rel="successor-version"
Answer after the sunset date410 Gone

TipClients must ignore unknown fields. Even an added field breaks a client that rejects them.

TrapDeprecation: true is an old draft form and no longer valid. Send a date that comes before the Sunset date.

Async & real-time

work that outlasts one request

Long jobs and webhooks

Start a job that takes minutes202 AcceptedPlus a status URL to poll.
Prove a webhook is yours and freshHMAC-sign the timestamp and raw body. Reject old timestamps.
Handle a receiver that's downRetry with backoff, then move the event to a dead-letter queue.
Handle the same event arriving twiceDedupe on the event id, and reply before slow work.

TrapWebhooks arrive at least once and in no fixed order. Receivers must expect duplicates.

Upload files

Upload a small fileContent-Type: multipart/form-data
Upload a large fileHand out a pre-signed URL. The bytes go straight to storage.

TipCheck the type, enforce a size limit and scan for viruses before anyone downloads it.

Architecture & scale

where the API sits in a bigger system

Services calling services

ordersstockpaymentshipping
orchestrationThe orders service calls each step in turn, and undoes them if one fails.
ordersstockpaymentshipping
choreographyNo coordinator. Each service reacts to the event the one before it published.
Decouple servicesA queue for one consumer, an event stream for many.
Run one flow across several servicesA saga. Local steps, with compensations to undo them on failure.
Choose strong or eventual consistencyStrong for money. Eventual for feeds and analytics.

gRPC basics

the contract, the wire format and the four call types

Define a gRPC service

Send one request, get one responserpc GetUser(Req) returns (User);
Send one request, get a stream backrpc List(Req) returns (stream User);
Stream requests, get one responserpc Upload(stream Chunk) returns (Res);
Stream both ways at oncerpc Chat(stream Msg) returns (stream Msg);
Generate Go stubs and message typesprotoc --go_out=. --go-grpc_out=. user.proto

TrapA field's number is its name on the wire. Never change or reuse one.

gRPC in production

keep calls fast, safe and easy to debug

Metadata, interceptors and auth

Run code around every callgrpc.UnaryInterceptor(f)
Send a token with every callPer-call credentials, sent as authorization metadata.
Make clients prove who they areMutual TLS, checked against your own CA, not system roots.
Reject bad requests before the handlerprotovalidate rules, enforced in an interceptor.

TrapUnary interceptors skip streaming calls. Register stream interceptors too.

Connections and load balancing

clientL4 proxyABC
per connectionAn L4 proxy picks a server per connection, so every call lands on A. B and C sit idle.
clientL7 proxyABC
per callAn L7 proxy, or the client itself, picks a server for each call.
Open one channel and share itgrpc.NewClient(addr, opts...)It replaces the deprecated grpc.Dial.
Spread calls over every server{"loadBalancingPolicy": "round_robin"}The default is pick first.
Tune for high throughputA few pooled channels, compression and keepalive.

TrapA plain Kubernetes Service gives one virtual IP, so every call goes to one pod. Use a headless Service.

Run and debug a gRPC server

Tell balancers a service is readyRegister the health service and set SERVING.
Shut down without dropping callsserver.GracefulStop()Set NOT_SERVING first, and call Stop if it hangs.
Trace calls across servicesAdd OpenTelemetry's otelgrpc stats handlers on both sides.

TipWhen a call fails, read its status code first, then turn up the logs with GRPC_GO_LOG_SEVERITY_LEVEL=info.

Docs, tests & monitoring

prove it works, and watch it in production

Document the contract

Keep docs true to the codeAn OpenAPI spec beside the code. CI fails when they differ.
Agree the API before anyone codes itAPI-first. Write the spec, then generate stubs and SDKs.
Prototype fastCode-first. Generate the docs from annotations in the code.

TipA public API also needs examples, SDKs, a sandbox and self-serve keys.

Found this useful? Pass it on.
Pro · $10/mo

This sheet stays free. With Pro, you can:

  • Every answer on this sheet. Read the full answer to all 92 questions it links, not just the 18 free ones.
  • My sheet. The cards you star and the ones you miss in Quiz me, from every sheet, on one page to quiz and print.
  • PDFs. Download this sheet and My sheet, laid out for A4, with every question a link to its answer.
  • The rest of Pro. Every answer in the question library, and 1,000 AI credits a month.