8. API Design
APIs (Application Programming Interfaces) define how services communicate with each other. Good API design is critical for building maintainable, scalable, and developer-friendly systems.
REST (Representational State Transfer)
The most widely used API style for web services. REST is an architectural style, not a protocol.
REST Principles
| Principle | Description |
|---|---|
| Client-Server | Separation of concerns; client handles UI, server handles data |
| Stateless | Each request contains all information needed; no server-side session |
| Cacheable | Responses must define themselves as cacheable or not |
| Uniform Interface | Consistent resource-based URLs, standard HTTP methods |
| Layered System | Client doesn't know if it talks to server directly or via intermediary |
| Code on Demand (optional) | Server can send executable code (e.g., JavaScript) |
HTTP Methods (Verbs)
| Method | Operation | Idempotent | Safe | Request Body |
|---|---|---|---|---|
| GET | Read a resource | Yes | Yes | No |
| POST | Create a resource | No | No | Yes |
| PUT | Replace a resource entirely | Yes | No | Yes |
| PATCH | Partially update a resource | No* | No | Yes |
| DELETE | Remove a resource | Yes | No | No |
| HEAD | Get headers only (no body) | Yes | Yes | No |
| OPTIONS | Get supported operations | Yes | Yes | No |
*PATCH can be made idempotent depending on implementation.
RESTful URL Design
# Resources (nouns, not verbs)
GET /users # List all users
GET /users/123 # Get user 123
POST /users # Create a new user
PUT /users/123 # Replace user 123
PATCH /users/123 # Update user 123 partially
DELETE /users/123 # Delete user 123
# Nested resources
GET /users/123/orders # List orders for user 123
GET /users/123/orders/456 # Get order 456 for user 123
POST /users/123/orders # Create order for user 123
# Filtering, sorting, pagination
GET /users?status=active&sort=name&page=2&limit=20
GET /orders?start_date=2025-01-01&end_date=2025-12-31
# Search
GET /users/search?q=alice
# Actions (when CRUD doesn't fit)
POST /orders/123/cancel # Cancel order 123
POST /users/123/verify-email # Trigger email verification
HTTP Status Codes
| Code | Meaning | When to Use |
|---|---|---|
| 200 | OK | Successful GET, PUT, PATCH, DELETE |
| 201 | Created | Successful POST (resource created) |
| 204 | No Content | Successful DELETE (no body) |
| 301 | Moved Permanently | Resource URL changed permanently |
| 304 | Not Modified | Cached response is still valid |
| 400 | Bad Request | Invalid request format/data |
| 401 | Unauthorized | Missing or invalid authentication |
| 403 | Forbidden | Authenticated but not authorized |
| 404 | Not Found | Resource doesn't exist |
| 405 | Method Not Allowed | HTTP method not supported for this resource |
| 409 | Conflict | Resource conflict (e.g., duplicate) |
| 422 | Unprocessable Entity | Valid format but invalid data |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Internal Server Error | Unexpected server error |
| 502 | Bad Gateway | Upstream service error |
| 503 | Service Unavailable | Server overloaded or under maintenance |
| 504 | Gateway Timeout | Upstream service timeout |
REST Response Format
// Success response
{
"data": {
"id": "user-123",
"name": "Alice",
"email": "alice@example.com",
"createdAt": "2025-01-15T10:30:00Z"
}
}
// List response with pagination
{
"data": [
{"id": "user-1", "name": "Alice"},
{"id": "user-2", "name": "Bob"}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 150,
"totalPages": 8,
"nextCursor": "eyJpZCI6InVzZXItMjAifQ=="
}
}
// Error response
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid email format",
"details": [
{
"field": "email",
"message": "Must be a valid email address"
}
]
}
}
Pagination Strategies
| Strategy | Pros | Cons |
|---|---|---|
Offset-based (?page=2&limit=20) |
Simple, allows jumping to any page | Slow for large offsets; inconsistent with concurrent writes |
Cursor-based (?cursor=abc&limit=20) |
Consistent, performant at any depth | Can't jump to arbitrary pages |
Keyset (?after_id=123&limit=20) |
Very fast (uses indexed column) | Only forward/backward navigation |
REST Versioning
| Strategy | Example | Pros | Cons |
|---|---|---|---|
| URL path | /v1/users, /v2/users |
Clear, easy to route | URL pollution |
| Query param | /users?version=2 |
Clean URLs | Easy to miss |
| Header | Accept: application/vnd.api.v2+json |
Clean URLs, proper HTTP | Hard to test in browser |
| Content negotiation | Accept: application/vnd.company.v2+json |
Most "RESTful" | Complex |
GraphQL
A query language for APIs developed by Facebook (2015). The client specifies exactly what data it needs.
Key Concepts
# Schema definition
type User {
id: ID!
name: String!
email: String!
orders: [Order!]!
}
type Order {
id: ID!
total: Float!
status: String!
items: [OrderItem!]!
}
type Query {
user(id: ID!): User
users(limit: Int, offset: Int): [User!]!
}
type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
}
type Subscription {
orderStatusChanged(orderId: ID!): Order!
}
# Client query — request exactly what you need
query {
user(id: "123") {
name
email
orders {
id
total
status
}
}
}
// Response — matches the query shape exactly
{
"data": {
"user": {
"name": "Alice",
"email": "alice@example.com",
"orders": [
{"id": "ord-1", "total": 99.99, "status": "shipped"},
{"id": "ord-2", "total": 49.50, "status": "pending"}
]
}
}
}
Advantages Over REST
| Problem | REST | GraphQL |
|---|---|---|
| Over-fetching | GET /users/123 returns ALL fields |
Client requests only needed fields |
| Under-fetching | Need multiple requests: /users/123 + /users/123/orders |
Single query gets everything |
| Versioning | /v1/users, /v2/users |
Add fields without breaking clients |
| Documentation | External (Swagger/OpenAPI) | Introspective (self-documenting schema) |
GraphQL Challenges
| Challenge | Description |
|---|---|
| N+1 problem | Resolving nested relations can trigger many DB queries. Solve with DataLoader (batching). |
| Complexity | Deep/nested queries can be expensive. Use query depth limiting and cost analysis. |
| Caching | Harder than REST (single endpoint, POST requests). Use persisted queries or client-side caching (Apollo). |
| File uploads | Not natively supported. Use multipart upload extensions. |
| Learning curve | Schema design, resolvers, type system — more concepts to learn. |
gRPC (Google Remote Procedure Call)
A high-performance RPC framework using Protocol Buffers (protobuf) for serialization and HTTP/2 for transport.
Protocol Buffers (Protobuf)
// user.proto
syntax = "proto3";
service UserService {
rpc GetUser (GetUserRequest) returns (User);
rpc ListUsers (ListUsersRequest) returns (stream User);
rpc CreateUser (CreateUserRequest) returns (User);
rpc Chat (stream ChatMessage) returns (stream ChatMessage);
}
message User {
string id = 1;
string name = 2;
string email = 3;
int32 age = 4;
}
message GetUserRequest {
string id = 1;
}
gRPC Communication Patterns
| Pattern | Description | Use Case |
|---|---|---|
| Unary | Single request, single response | Standard API calls |
| Server streaming | Single request, stream of responses | Real-time feeds, large result sets |
| Client streaming | Stream of requests, single response | File upload, telemetry |
| Bidirectional streaming | Stream both ways | Chat, real-time collaboration |
Unary: Client ──req──→ Server ──res──→ Client
Server streaming: Client ──req──→ Server ══res══▷ Client (multiple responses)
Client streaming: Client ══req══▷ Server ──res──→ Client
Bidirectional: Client ◁══════▷ Server (both stream simultaneously)
gRPC Advantages
- Performance: Binary serialization (protobuf) is 5-10x faster than JSON.
- HTTP/2: Multiplexing, header compression, bi-directional streaming.
- Strong typing: Protobuf schema generates type-safe client/server code.
- Code generation: Auto-generate clients in 10+ languages from
.protofiles. - Streaming: Native support for streaming data.
gRPC Limitations
- Browser support: Limited (needs gRPC-Web proxy).
- Human readability: Binary format — can't inspect with
curl. - Tooling: Less mature ecosystem than REST.
- Load balancing: L7 load balancers must understand HTTP/2.
WebSocket
A protocol for full-duplex, persistent communication over a single TCP connection.
Client ←→ Server (persistent bidirectional connection)
1. Client sends HTTP Upgrade request
2. Server responds with 101 Switching Protocols
3. Both sides can send messages at any time
Use cases: Real-time chat, live dashboards, multiplayer games, stock tickers, collaborative editing.
Comparison with alternatives:
| Feature | HTTP Polling | Long Polling | SSE | WebSocket |
|---|---|---|---|---|
| Direction | Client → Server | Client → Server | Server → Client | Bidirectional |
| Latency | High (interval) | Medium | Low | Very low |
| Connection | New per request | Held open | Held open | Persistent |
| Overhead | High | Medium | Low | Very low |
| Protocol | HTTP | HTTP | HTTP | WS (TCP) |
Server-Sent Events (SSE)
A standard for server-to-client unidirectional streaming over HTTP.
Client ──GET /events──→ Server
Client ◁──event: update\ndata: {...}──
Client ◁──event: update\ndata: {...}──
Client ◁──event: update\ndata: {...}──
Use cases: Live notifications, stock prices, news feeds — where only server-to-client is needed.
REST vs GraphQL vs gRPC
| Aspect | REST | GraphQL | gRPC |
|---|---|---|---|
| Protocol | HTTP/1.1 or HTTP/2 | HTTP (usually POST) | HTTP/2 |
| Data format | JSON | JSON | Protobuf (binary) |
| Schema | OpenAPI (optional) | Required (SDL) | Required (.proto) |
| Typing | Weak | Strong | Strong |
| Performance | Good | Good | Excellent |
| Browser support | Excellent | Good | Limited |
| Streaming | Limited (SSE, WS) | Subscriptions | Native |
| Caching | Easy (HTTP caching) | Complex | Complex |
| Learning curve | Low | Medium | Medium-High |
| Best for | Public APIs, CRUD | Flexible client queries | Internal microservices |
API Design Best Practices
1. Use Proper HTTP Semantics
- Correct methods (GET for reads, POST for creates).
- Correct status codes (don't return 200 for errors).
- Correct headers (Content-Type, Cache-Control).
2. Design for Idempotency
- PUT and DELETE should be idempotent.
- Use idempotency keys for POST:
POST /payments Idempotency-Key: unique-request-id-123
3. Handle Errors Consistently
{
"error": {
"code": "INSUFFICIENT_FUNDS",
"message": "Account balance is too low",
"requestId": "req-abc-123",
"timestamp": "2025-01-15T10:30:00Z"
}
}
4. Rate Limiting Headers
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 45
X-RateLimit-Reset: 1705312800
Retry-After: 30
5. API Security
- Authentication: OAuth 2.0, API keys, JWT.
- Authorization: Role-based access control (RBAC).
- HTTPS: Always encrypt in transit.
- Input validation: Validate all inputs server-side.
- Rate limiting: Protect against abuse.
6. Documentation
- OpenAPI/Swagger for REST.
- GraphQL schema introspection for GraphQL.
- Protobuf definitions for gRPC.
API Gateway
A single entry point that routes requests to appropriate backend services.
Client → [API Gateway] → /users/* → User Service
→ /orders/* → Order Service
→ /payments/* → Payment Service
Responsibilities:
- Request routing
- Authentication & authorization
- Rate limiting & throttling
- Request/response transformation
- Caching
- Logging & monitoring
- SSL termination
- API versioning
Technologies: Kong, AWS API Gateway, Apigee, Nginx, Envoy, Traefik.
Summary
| Concept | Key Point |
|---|---|
| REST | Simple, widely supported, cacheable — best for public APIs |
| GraphQL | Flexible queries, no over/under-fetching — best for complex UIs |
| gRPC | High performance, streaming, strong typing — best for internal services |
| WebSocket | Bidirectional real-time — best for chat, games, live updates |
| SSE | Server-to-client streaming — best for notifications, feeds |
| API Gateway | Single entry point for routing, auth, rate limiting |
Rule of thumb: Use REST for public APIs and simple CRUD. Use GraphQL for complex client-driven queries (especially mobile). Use gRPC for internal service-to-service communication where performance matters.