Skip to content

API Design

An API (Application Programming Interface) is the contract between a service and its consumers. A well-designed API is intuitive to use, hard to misuse, and stable enough to depend on. A poorly designed one becomes a long-term maintenance burden that’s impossible to change without breaking clients.

This page covers the dominant API paradigms, design best practices, and the operational concerns (versioning, rate limiting, auth) that every production API requires.


REST is the dominant architectural style for HTTP APIs. It’s not a protocol or a standard — it’s a set of constraints that guide how you model resources and use HTTP semantics.

  • Stateless: Each request contains all information needed to process it. No server-side session state between requests.
  • Uniform Interface: Resources are identified by URIs. Interactions use standard HTTP methods (verbs) with consistent semantics.
  • Client-Server: The client and server evolve independently. The API is the contract between them.
  • Cacheable: Responses declare whether they can be cached. GET responses are cacheable by default; POST/DELETE are not.
MethodOperationIdempotent?Safe?
GETRetrieve a resource
POSTCreate a new resource
PUTReplace a resource entirely
PATCHPartially update a resource❌*
DELETEDelete a resource
  • Idempotent: Calling the same operation N times produces the same result as calling it once
  • Safe: The operation has no side effects (read-only)
# Good: noun-based, hierarchical, lowercase-kebab-case
GET /users # List all users
POST /users # Create a user
GET /users/42 # Get user 42
PATCH /users/42 # Update user 42
DELETE /users/42 # Delete user 42
GET /users/42/orders # List orders for user 42
GET /users/42/orders/7 # Get specific order
# Bad: verbs in URL, inconsistent casing
GET /getUser?id=42
POST /createNewOrder
GET /User_Orders/42
RangeCategoryCommon Examples
2xxSuccess200 OK, 201 Created, 204 No Content
3xxRedirect301 Moved Permanently, 304 Not Modified
4xxClient Error400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 422 Unprocessable Entity, 429 Too Many Requests
5xxServer Error500 Internal Server Error, 502 Bad Gateway, 503 Service Unavailable

gRPC is a high-performance RPC framework developed by Google, built on HTTP/2 and Protocol Buffers (protobuf) — a binary serialization format.

// Service definition in .proto file
service UserService {
rpc GetUser(GetUserRequest) returns (User);
rpc CreateUser(CreateUserRequest) returns (User);
rpc StreamUserEvents(UserEventsRequest) returns (stream UserEvent);
}
message GetUserRequest {
int64 user_id = 1;
}
AspectREST (JSON/HTTP 1.1)gRPC (Protobuf/HTTP 2)
PayloadJSON (text, human-readable)Binary protobuf (compact, fast)
PerformanceGood~5-10x faster serialization
StreamingWorkarounds (SSE, WebSocket)Native (client, server, bidirectional)
Type safetyNone (schema optional)Strict, enforced by proto definition
Browser supportNativeRequires gRPC-Web proxy
Human readabilityEasy to debug with curl/PostmanRequires tooling (gRPCurl, Postman)

When to use gRPC:

  • Internal service-to-service communication where performance matters
  • Streaming scenarios (real-time event feeds, large data transfers)
  • Polyglot microservices (protobuf generates clients for any language)

When to use REST:

  • Public APIs consumed by browsers or external clients
  • Teams that need easy debuggability

GraphQL is a query language for APIs that lets clients request exactly the data they need — no more, no less.

# Client specifies exactly what fields they want
query {
user(id: "42") {
name
email
orders(first: 5) {
id
total
status
}
}
}
ProblemRESTGraphQL
Over-fetchingGET /users/42 returns all user fields even if you only need nameClient requests only name
Under-fetchingNeed 3 requests: /users/42, /users/42/orders, /products/7Single request with nested query
Schema documentationOptional (OpenAPI/Swagger)Introspection built-in
Type safetyOptionalBuilt-in (schema is the contract)
CachingHTTP caching built-inComplex (queries are POST; cache by query hash)

When to use GraphQL:

  • Public APIs consumed by multiple clients with different data needs (mobile vs. web)
  • Teams that want to avoid API versioning by making all fields optional

When to avoid GraphQL:

  • Simple CRUD services — the overhead of schema and resolver setup isn’t worth it
  • Teams without the bandwidth to manage N+1 query problems (use DataLoader to batch DB calls)

APIs change. Versioning is how you evolve them without breaking existing clients.

StrategyExampleNote
URL Path/v1/users, /v2/usersMost explicit; easy to route at the load balancer level
HeaderAccept: application/vnd.api+json;version=2Cleaner URLs; harder to test in a browser
Query Param/users?version=2Simple but pollutes query params
Subdomainv2.api.example.comRare; requires DNS management

URL path versioning is the most common and safest choice. It’s explicit, visible in logs, and trivial to route.

Before bumping versions, prefer backward-compatible changes:

  • Adding new optional fields to responses: always safe
  • Adding new endpoints: always safe
  • Deprecating fields: mark as deprecated in docs; don’t remove for at least one major version cycle
  • Removing or renaming fields: breaking change — requires a version bump

Rate limiting protects your API from abuse, prevents runaway clients from degrading service for everyone, and enforces pricing tiers for paid APIs.

AlgorithmHowBehavior
Fixed WindowCount requests in a fixed time window (e.g., 100 req per 60s)Simple; vulnerable to bursting at window boundary
Sliding WindowCount requests in a rolling time windowSmoother; prevents boundary bursting
Token BucketRefill tokens at a fixed rate; each request consumes one tokenAllows bursting up to bucket size
Leaky BucketQueue requests and process at a fixed rateSmooths traffic; introduces latency
HTTP/1.1 429 Too Many Requests
Retry-After: 30
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1711027260
{
"error": "rate_limit_exceeded",
"message": "Too many requests. Retry after 30 seconds."
}

API Keys:

  • Long-lived secret string passed in a header (Authorization: Bearer <key> or X-API-Key: <key>)
  • Simple to issue; hard to revoke without rotating the key
  • Use for: server-to-server, low-sensitivity read-only access, developer sandboxes

JWT (JSON Web Tokens):

  • Signed, self-contained tokens that encode claims (user ID, roles, expiry)
  • The server validates the signature — no DB lookup required per request
  • Stateless: Revocation requires either short-lived tokens or a token deny-list
  • Use for: user authentication in stateless APIs, microservice-to-microservice auth
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI0MiJ9.signature
[header] [payload] [signature]

OAuth 2.0:

  • Delegation protocol: lets users grant third-party apps access to their resources without sharing credentials
  • Flows: Authorization Code (browser), Client Credentials (server-to-server), PKCE (mobile/SPA)
  • Use for: “Sign in with Google”, third-party integrations, delegated access

  • Resources are nouns, not verbs; URLs are lowercase-kebab-case
  • HTTP methods align with their semantic meaning (GET = read, POST = create, etc.)
  • All error responses include a human-readable message and machine-readable error code
  • Pagination implemented on all list endpoints (cursor, limit/offset, or page)
  • Rate limiting in place with informative response headers
  • Auth documented and enforced consistently
  • Versioning strategy decided before first public consumer
  • Breaking changes require a version bump, never a silent behavior change
  • OpenAPI / Swagger spec maintained alongside code