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 (Representational State Transfer)
Section titled “REST (Representational State Transfer)”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.
Core Constraints
Section titled “Core Constraints”- 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.
GETresponses are cacheable by default;POST/DELETEare not.
HTTP Methods and Semantics
Section titled “HTTP Methods and Semantics”| Method | Operation | Idempotent? | Safe? |
|---|---|---|---|
GET | Retrieve a resource | ✅ | ✅ |
POST | Create a new resource | ❌ | ❌ |
PUT | Replace a resource entirely | ✅ | ❌ |
PATCH | Partially update a resource | ❌* | ❌ |
DELETE | Delete 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)
URL Design
Section titled “URL Design”# Good: noun-based, hierarchical, lowercase-kebab-caseGET /users # List all usersPOST /users # Create a userGET /users/42 # Get user 42PATCH /users/42 # Update user 42DELETE /users/42 # Delete user 42GET /users/42/orders # List orders for user 42GET /users/42/orders/7 # Get specific order
# Bad: verbs in URL, inconsistent casingGET /getUser?id=42POST /createNewOrderGET /User_Orders/42HTTP Status Codes
Section titled “HTTP Status Codes”| Range | Category | Common Examples |
|---|---|---|
| 2xx | Success | 200 OK, 201 Created, 204 No Content |
| 3xx | Redirect | 301 Moved Permanently, 304 Not Modified |
| 4xx | Client Error | 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 422 Unprocessable Entity, 429 Too Many Requests |
| 5xx | Server Error | 500 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 fileservice UserService { rpc GetUser(GetUserRequest) returns (User); rpc CreateUser(CreateUserRequest) returns (User); rpc StreamUserEvents(UserEventsRequest) returns (stream UserEvent);}
message GetUserRequest { int64 user_id = 1;}gRPC vs REST
Section titled “gRPC vs REST”| Aspect | REST (JSON/HTTP 1.1) | gRPC (Protobuf/HTTP 2) |
|---|---|---|
| Payload | JSON (text, human-readable) | Binary protobuf (compact, fast) |
| Performance | Good | ~5-10x faster serialization |
| Streaming | Workarounds (SSE, WebSocket) | Native (client, server, bidirectional) |
| Type safety | None (schema optional) | Strict, enforced by proto definition |
| Browser support | Native | Requires gRPC-Web proxy |
| Human readability | Easy to debug with curl/Postman | Requires 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
Section titled “GraphQL”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 wantquery { user(id: "42") { name email orders(first: 5) { id total status } }}GraphQL vs REST
Section titled “GraphQL vs REST”| Problem | REST | GraphQL |
|---|---|---|
| Over-fetching | GET /users/42 returns all user fields even if you only need name | Client requests only name |
| Under-fetching | Need 3 requests: /users/42, /users/42/orders, /products/7 | Single request with nested query |
| Schema documentation | Optional (OpenAPI/Swagger) | Introspection built-in |
| Type safety | Optional | Built-in (schema is the contract) |
| Caching | HTTP caching built-in | Complex (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)
API Versioning
Section titled “API Versioning”APIs change. Versioning is how you evolve them without breaking existing clients.
Versioning Strategies
Section titled “Versioning Strategies”| Strategy | Example | Note |
|---|---|---|
| URL Path | /v1/users, /v2/users | Most explicit; easy to route at the load balancer level |
| Header | Accept: application/vnd.api+json;version=2 | Cleaner URLs; harder to test in a browser |
| Query Param | /users?version=2 | Simple but pollutes query params |
| Subdomain | v2.api.example.com | Rare; requires DNS management |
URL path versioning is the most common and safest choice. It’s explicit, visible in logs, and trivial to route.
Backward Compatibility
Section titled “Backward Compatibility”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
Section titled “Rate Limiting”Rate limiting protects your API from abuse, prevents runaway clients from degrading service for everyone, and enforces pricing tiers for paid APIs.
Rate Limiting Strategies
Section titled “Rate Limiting Strategies”| Algorithm | How | Behavior |
|---|---|---|
| Fixed Window | Count requests in a fixed time window (e.g., 100 req per 60s) | Simple; vulnerable to bursting at window boundary |
| Sliding Window | Count requests in a rolling time window | Smoother; prevents boundary bursting |
| Token Bucket | Refill tokens at a fixed rate; each request consumes one token | Allows bursting up to bucket size |
| Leaky Bucket | Queue requests and process at a fixed rate | Smooths traffic; introduces latency |
Rate Limit Response
Section titled “Rate Limit Response”HTTP/1.1 429 Too Many RequestsRetry-After: 30X-RateLimit-Limit: 100X-RateLimit-Remaining: 0X-RateLimit-Reset: 1711027260
{ "error": "rate_limit_exceeded", "message": "Too many requests. Retry after 30 seconds."}Authentication & Authorization
Section titled “Authentication & Authorization”Common Patterns
Section titled “Common Patterns”API Keys:
- Long-lived secret string passed in a header (
Authorization: Bearer <key>orX-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
API Design Checklist
Section titled “API Design Checklist”- 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
messageand machine-readableerrorcode - Pagination implemented on all list endpoints (
cursor,limit/offset, orpage) - 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