Skip to main content

Reference: API Design Patterns

A short reference on REST vs gRPC, versioning, pagination, idempotency, and the small decisions that separate a designed API from an improvised one.

What it is

The contract between a service and its callers. Good API design decides six things: protocol (REST vs gRPC vs others), resource modeling, versioning, pagination, idempotency, and how identity and context flow through requests.

When you care

Every system design interview has an API section. Candidates who name three endpoints and move on lose signal to candidates who call out idempotency keys, pagination style, and versioning strategy in the same breath. The cheap wins live here.

REST vs gRPC

DimensionRESTgRPC
Wire formatJSON over HTTP/1.1 or HTTP/2Protobuf over HTTP/2
SchemaOpenAPI (optional).proto file (required)
CodegenOptionalNative, multi-language
Browser supportNativeRequires gRPC-Web proxy
StreamingSSE or WebSocket as sidecarBuilt-in (client, server, bidi)
Human-readableYesNo (binary)
VersioningURL or header-basedProto field numbers
Good forPublic APIs, browser clients, third-party integrationsInternal service-to-service, polyglot backends, streaming

REST is the default for anything a browser or third party will call. gRPC is the default for internal microservice traffic where both sides are under your control. Most production systems use both — REST at the edge, gRPC inside.

RESTful resource modeling

  • Nouns, not verbs. POST /orders, not POST /createOrder.
  • Plural resource names. /users/123, not /user/123.
  • Hierarchy reflects ownership. /users/123/orders for user-scoped resources; top-level /orders for globally addressable ones.
  • Status codes carry meaning. 201 Created on successful POST, 204 No Content on DELETE, 409 Conflict on idempotency mismatch, 422 Unprocessable Entity on validation failure.

Identity from context, not parameters

Never accept user_id as a request parameter on endpoints that act on the caller. Derive it from the authenticated session.

BAD:  GET /api/orders?user_id=123
GOOD: GET /api/orders        (user_id derived from auth token)

user_id as a parameter invites authorization bugs — someone changes 123 to 124 and sees another user’s orders. Sensitive identifiers come from the auth context, not the request body. The parameter-based form is only acceptable when the caller legitimately acts on behalf of another user (admin endpoints, support tools) and that authorization is explicitly checked.

Versioning

StrategyFormTradeoff
URL versioning/v1/orders, /v2/ordersExplicit, visible, easy to route. Breaks link permanence across versions.
Header versioningAccept: application/vnd.api+json; version=2URLs stay stable. Harder to debug; invisible in logs.
Parameter versioning/orders?version=2Rarely used; mixes versioning with query semantics.
NeverAlways breakingDon’t.

URL versioning is the default for public APIs. Bump major versions for breaking changes; add optional fields for non-breaking additions. gRPC uses proto field numbers for the same purpose — adding new fields is backward-compatible by design.

Pagination

StyleRequestResponseGood for
Offset / limit?offset=100&limit=20Items + total countSmall datasets, random-access UIs (page 47).
Cursor-based?cursor=abc123&limit=20Items + next_cursorLarge datasets, feeds, infinite scroll.
Keyset (seek)?after_id=999&limit=20Items (client uses last ID)Monotonically ordered data; highest performance.

Offset pagination is simple but breaks at scale. OFFSET 1,000,000 on a DB is an O(N) scan. It also returns inconsistent results when the underlying data changes mid-pagination.

Cursor pagination encodes the position as an opaque token (often a signed, base64-encoded row ID + sort key). Handles insertions gracefully, scales cleanly, and hides implementation details from the client. The default choice for any feed, list, or log.

Keyset pagination is cursor pagination with a known structure (usually the primary key). Fastest option when the ordering matches the index. Used heavily in internal systems.

Idempotency

An idempotent API produces the same observable result when called once or multiple times with the same input. Idempotency is a correctness property for network retries — if the client retries a payment because a response timed out, you do not want two payments.

MethodIdempotent?Notes
GET, HEADYesNatural.
PUTYesReplaces the resource; same PUT = same state.
DELETEYesSecond DELETE returns 404 or 204, not an error.
POSTNot by defaultMake it idempotent via idempotency keys.
PATCHDependsOnly if the patch is a replace, not an increment.

Idempotency keys. For non-idempotent operations (mostly POST), the client generates a unique key (UUID) per logical operation and sends it as a header: Idempotency-Key: abc-123. The server stores a mapping of key → result for some window (commonly 24 hours). On retry, it returns the stored result without re-executing.

Every non-idempotent endpoint that matters (payments, order creation, transfers, message sends) should accept an idempotency key. This is a small design decision with a large correctness payoff, and it’s one of the cheapest signals to display in an interview.

Common practices worth naming

  • Consistent error shape. One envelope across all errors: { error: { code, message, details } }. Clients parse once.
  • Field naming. Pick snake_case or camelCase and never mix. snake_case is the REST/JSON convention in most style guides.
  • Timestamps are ISO 8601 strings, UTC. 2026-05-08T14:30:00Z. Numeric Unix timestamps invite timezone bugs.
  • Rate limiting at the edge. 429 Too Many Requests with Retry-After header. Belongs in a gateway, not per-endpoint logic.
  • Null vs missing. Decide whether absent fields mean “no value” or “don’t change” (PATCH). Document it.

When to pick what

  • Public API, browser clients, third-party integrations: REST with URL versioning, cursor pagination, idempotency keys on all non-GET endpoints.
  • Internal microservice traffic: gRPC with protobuf; use REST only at the edge.
  • Streaming: gRPC streaming for internal; SSE or WebSocket for browser-facing.
  • Operations that modify state: PUT if idempotent by design, POST with idempotency key otherwise.