APIs — Deep Dive

API Design Paradigms

The API landscape has fractured into several competing paradigms, each with real tradeoffs.

REST (Representational State Transfer)

REST became dominant because it maps naturally onto HTTP — the protocol already running the web. Roy Fielding formalized it in his 2000 dissertation, but it took years for the industry to coalesce around it.

Core constraints:

  • Stateless — Each request must contain all information needed to process it. The server stores no session state between requests.
  • Uniform Interface — Resources are identified by URIs; HTTP verbs define actions
  • Layered System — Clients don’t know if they’re talking to the actual server or a cache/proxy
  • Cacheable — Responses must declare whether they’re cacheable

The stateless constraint is frequently misunderstood. It doesn’t mean no state exists — it means state lives in the database or client, not on the application server. This is what makes REST APIs horizontally scalable: any server in the pool can handle any request.

A well-designed REST resource hierarchy:

GET    /users/{id}              ← Fetch user
PATCH  /users/{id}              ← Partial update
GET    /users/{id}/orders       ← User's orders
POST   /users/{id}/orders       ← Create order for user
GET    /users/{id}/orders/{oid} ← Specific order
DELETE /users/{id}/orders/{oid} ← Cancel order

Notice: nested resources reflect ownership relationships. The URL is the resource’s identity.

GraphQL

Facebook built GraphQL internally in 2012 and open-sourced it in 2015 to solve a specific problem: mobile clients on slow connections were getting bloated REST responses and required multiple round-trips to fetch related data.

With REST, fetching a user’s name, their last 3 posts, and each post’s comment count might require:

  1. GET /users/123
  2. GET /users/123/posts?limit=3
  3. GET /posts/1/comments/count, GET /posts/2/comments/count, GET /posts/3/comments/count

With GraphQL, one query:

query {
  user(id: "123") {
    name
    posts(last: 3) {
      title
      commentCount
    }
  }
}

The response contains exactly the fields requested — no over-fetching, one round trip.

The tradeoffs:

  • Caching is harder (POST requests with query bodies don’t cache like GET URLs)
  • Query complexity can be weaponized — a malicious actor can craft deeply nested queries that DoS your database (mitigated with query depth limits and complexity scoring)
  • Schema introspection leaks your data model to attackers if not restricted

GitHub, Shopify, and Twitter/X all expose GraphQL APIs for their developer platforms.

gRPC

Google’s internal RPC framework, open-sourced in 2016. Built on HTTP/2 and Protocol Buffers (a binary serialization format).

Why it’s faster than REST+JSON:

  • Protocol Buffers are ~5-10x more compact than JSON
  • HTTP/2 multiplexes multiple calls over a single connection
  • Streaming is first-class: server-streaming, client-streaming, bidirectional
service UserService {
  rpc GetUser (UserRequest) returns (UserResponse);
  rpc StreamUserEvents (UserRequest) returns (stream Event);
}

The tradeoffs:

  • Not human-readable (binary format makes debugging harder)
  • Poor browser support (gRPC-Web exists but is a workaround)
  • Tighter coupling — both sides must share the .proto schema

gRPC is the dominant choice for internal microservice communication at scale. Kubernetes, etcd, and most Google internal services use it.

API Versioning Strategies

APIs evolve. How you handle versioning determines how much you break your users.

URL versioninghttps://api.stripe.com/v1/charges The most common approach. Simple, visible, easy to route. Stripe uses it and explicitly dates their API versions (e.g., 2023-10-16).

Header versioningAccept: application/vnd.myapi.v2+json RESTfully pure (the URL identifies the resource, not the version) but awkward in practice. Hard to test in a browser.

Query parameter?api_version=2 Easy to test, but query params feel wrong for something that changes the API contract fundamentally.

The hard truth: Stripe’s model (date-based versioning + customer-pinned versions) is the gold standard. When they release a breaking change, existing customers stay on their current version until they explicitly upgrade. New customers get the latest. This requires maintaining multiple versions simultaneously — expensive, but it’s what a $95B company does to not break its customers.

Rate Limiting

Every production API rate limits. The implementation matters.

Token bucket — Each client gets a bucket with a capacity (e.g., 100 requests) that refills at a constant rate (e.g., 10/second). Allows brief bursts. Used by most cloud providers.

Leaky bucket — Requests queue and process at a fixed rate. Smooths traffic but queues can back up.

Fixed window — Count requests per fixed time window (e.g., 1,000/hour). Simple but vulnerable to thundering herd at window boundaries.

Sliding window — More accurate, higher computational cost. Avoids the boundary problem.

Standard response headers for rate limit state:

X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 847
X-RateLimit-Reset: 1711490400
Retry-After: 60

Clients that don’t respect Retry-After get exponential backoff forced on them or blocked entirely.

Pagination

Returning unbounded result sets breaks clients. Three common patterns:

Offset pagination:

GET /orders?offset=100&limit=50

Simple, but degrades with large offsets (the database scans offset + limit rows). Breaks when items are inserted/deleted mid-traversal.

Cursor pagination:

GET /orders?after=cursor_eyJpZCI6MTAwfQ&limit=50

The cursor encodes the position (often a base64’d {id: 100}). Fast for deep pagination, stable under mutations. Used by Slack, Twitter, Stripe for their feeds.

Keyset pagination:

GET /orders?after_id=100&limit=50

Similar to cursor but uses a real column value. Requires that column to be indexed and sortable.

Facebook’s Graph API uses cursor pagination across the board. Their next and previous cursor values encode server-side state, making it opaque to clients — they just follow the links.

Idempotency

A critical property for payment and mutation APIs: the same request applied multiple times has the same effect as applying it once.

GET requests are naturally idempotent. POST requests are not — unless you design them to be.

Stripe’s idempotency key pattern:

POST /v1/charges
Idempotency-Key: a84a8e7e-5e5a-4a2f-9f12-3b1234567890

{ "amount": 2000, "currency": "usd" }

If the network drops after Stripe processes the charge but before your client receives the response, retrying with the same key returns the original charge — not a duplicate. The key is stored server-side for 24 hours.

This pattern should exist in any API where the consequence of a duplicate call is worse than a cache lookup.

Security Considerations

OWASP API Security Top 10 highlights the most common failures:

  1. Broken Object Level Authorization — User A fetches User B’s data by guessing IDs (/orders/1337 when they should only see their own)
  2. Broken Authentication — Weak tokens, no expiry, tokens in URLs (which land in server logs)
  3. Excessive Data Exposure — Returning full objects and filtering in the frontend. Attackers see the raw API response.
  4. Mass Assignment — Accepting all fields in a POST body and binding them directly to the model. A user could set is_admin: true.

Practical defenses:

  • Always authorize at the object level, not just the endpoint
  • Use short-lived JWTs (15 min access tokens + refresh tokens)
  • Never return fields the client doesn’t need
  • Whitelist accepted fields explicitly

What Separates Good APIs from Infuriating Ones

After building on dozens of APIs, patterns emerge:

Good APIs:

  • Stripe’s error responses are works of art — machine-readable error codes, human-readable messages, links to documentation
  • Twilio uses subresources consistently and never surprises you with undocumented behavior
  • GitHub’s REST and GraphQL APIs are both fully documented with runnable examples

Painful APIs:

  • Inconsistent naming (userId vs user_id vs user-id in the same API)
  • Errors that return HTTP 200 with {"success": false} in the body (makes automated error handling a nightmare)
  • No pagination on list endpoints (works fine at 100 records, destroys servers at 1M)
  • Undocumented rate limits discovered in production

One Thing to Remember

API design is product design. The decisions you make — how you name resources, how you handle errors, how you version, how you paginate — determine whether developers choose your platform or your competitor’s. The best APIs are the ones where you almost never have to read the documentation.

techprogrammingwebrestgraphqlgrpcarchitecture

See Also

  • Encryption Encryption explained: how your messages and passwords stay secret even when strangers can see them.
  • Git Why do millions of programmers obsess over a tool that saves old versions of their work? Because without it, one bad day can delete months of effort.
  • Graphql Why do apps ask for exactly the data they need — and why that's a bigger deal than it sounds?