API Design
API Design
An API is a product. Its users are developers. Like any product, a good API reduces friction: developers can build what they need without reading long documentation, make mistakes difficult, and guide users toward correct usage through its design.
REST Principles
REST (Representational State Transfer) is an architectural style, not a protocol. Key constraints:
Resources, not actions: REST models state as resources with stable URLs. /orders/123 not /getOrderById?id=123. Resources are nouns; HTTP verbs express actions.
HTTP verb semantics:
GET: retrieve; safe (no side effects) and idempotentPOST: create or trigger an action; neither safe nor idempotentPUT: replace a resource entirely; idempotentPATCH: partial update; not necessarily idempotentDELETE: remove; idempotent
Statelessness: each request contains all information needed to process it. Session state is not stored on the server. This enables horizontal scaling: any server can handle any request.
Status codes with meaning:
- 200 OK, 201 Created, 204 No Content
- 400 Bad Request (client error, malformed request), 401 Unauthorized (authentication needed), 403 Forbidden (authenticated but not authorized), 404 Not Found, 409 Conflict (optimistic concurrency violation), 422 Unprocessable Entity (validation errors)
- 500 Internal Server Error (server bug), 503 Service Unavailable (dependency down), 429 Too Many Requests (rate limited)
Using 200 for everything is wrong: an error wrapped in a 200 response breaks HTTP-level error handling, caching, and monitoring.
REST API Design Practices
Consistent naming: plural nouns (/users, /orders), lowercase, hyphenated (/shipping-addresses). Consistent across the API.
Resource hierarchy: nested paths for truly hierarchical resources (/users/123/orders) but avoid deep nesting (/users/123/orders/456/items/789/discounts — start returning a resource directly at an appropriate level with IDs for context).
Filtering, sorting, pagination: /orders?status=pending&sort=created_at&order=desc&limit=20&cursor=<token>.
Versioning strategy: URL versioning (/v1/users) is most visible and explicit. Header versioning is "purer" REST but harder to test and debug. Avoid query parameter versioning (version state in URLs is standard; in query strings it's noise).
Error responses: return structured error bodies:
{ "error": { "code": "VALIDATION_FAILED", "message": "Email is invalid", "field": "email" } }Not just status codes. Clients need enough information to show a useful error message or retry correctly.
GraphQL
GraphQL (Facebook, 2015) is a query language for APIs and a runtime for executing those queries. Clients request exactly the fields they need.
Over-fetching and under-fetching: REST endpoints return fixed shapes. A /users/123 endpoint always returns all user fields even if you only need the name. A /users/123 endpoint might not return the user's recent orders, requiring a second request (N+1 problem). GraphQL solves both: request exactly what you need in one query.
Type system: GraphQL schemas are strongly typed. Every field has a type. Tools (GraphiQL, introspection) enable self-documentation and IDE autocompletion.
Queries vs Mutations: queries read data; mutations change it. Both return a response. Subscriptions are real-time updates over WebSocket.
GraphQL trade-offs:
- Complexity: caching is harder (queries are unique strings), rate limiting is harder (one endpoint, variable cost per query), authorization logic is distributed across resolvers
- N+1 problem in resolvers: loading related entities naively triggers N queries (one per parent). Requires DataLoader (batching and caching)
- Over-fetching to under: mobile clients benefit; internal APIs between services often don't need the flexibility
gRPC and Protocol Buffers
gRPC is a high-performance RPC framework using HTTP/2 and Protocol Buffer serialization.
Protocol Buffers: language-neutral, platform-neutral, binary serialization format. Faster and smaller than JSON. Strongly typed schemas shared between client and server.
Streaming: gRPC supports server streaming, client streaming, and bidirectional streaming.
When gRPC fits: internal service-to-service communication where performance and type safety matter more than human-readable messages. Not ideal for browser clients (gRPC-web adds friction).
API Design Principles
Pit of success: design the API so the easy path is the correct path. createOrder() requiring explicit idempotency keys makes safe retries the default. deleteUser(userId, {hard: true}) makes soft delete the default.
Consistency over cleverness: the most important property is consistency. If some endpoints return {"data": [...], "total": 100} and others return {"items": [...]}, clients need special cases everywhere.
Progressive disclosure: simple things should be simple; complex things should be possible. A basic request with sensible defaults; optional parameters for advanced use.
Design for change: expect fields to be added. Clients should ignore unknown fields (additive backward compatibility). Removing fields requires versioning.
Documentation is part of the product: an API without clear documentation, examples, and error descriptions is incomplete. Tools like OpenAPI/Swagger for REST, GraphQL introspection, and protobuf IDL for gRPC enable auto-generated documentation.