API Design Patterns
A comprehensive guide to designing robust, scalable, and maintainable APIs using modern patterns and best practices.
Overview
API design patterns provide standardised approaches for building interfaces that enable communication between software systems. Well-designed APIs improve developer experience, system reliability, and long-term maintainability.
graph TB
subgraph "API Architecture Overview"
Client[Client Applications]
subgraph "API Gateway Layer"
Gateway[API Gateway]
Auth[Authentication]
Rate[Rate Limiting]
Cache[Caching]
end
subgraph "API Styles"
REST[REST APIs]
GraphQL[GraphQL APIs]
gRPC[gRPC APIs]
end
subgraph "Backend Services"
Service1[Service A]
Service2[Service B]
Service3[Service C]
end
subgraph "Data Layer"
DB[Database]
MQ[Message Queue]
end
end
Client --> Gateway
Gateway --> Auth
Gateway --> Rate
Gateway --> Cache
Auth --> REST
Auth --> GraphQL
Auth --> gRPC
REST --> Service1
GraphQL --> Service2
gRPC --> Service3
Service1 --> DB
Service2 --> DB
Service3 --> MQ
RESTful Principles
Key Concepts
| Concept | Description |
|---|---|
| Resource-Oriented | URLs represent resources (nouns), not actions |
| Stateless | Each request contains all information needed |
| Uniform Interface | Consistent use of HTTP methods and status codes |
| HATEOAS | Hypermedia as the Engine of Application State |
| Cacheable | Responses indicate cacheability |
| Layered System | Client cannot tell if connected directly to server |
HTTP Methods
| Method | Purpose | Idempotent | Safe |
|---|---|---|---|
GET |
Retrieve resource | Yes | Yes |
POST |
Create resource | No | No |
PUT |
Replace resource | Yes | No |
PATCH |
Partial update | No | No |
DELETE |
Remove resource | Yes | No |
OPTIONS |
Get allowed methods | Yes | Yes |
HEAD |
Get headers only | Yes | Yes |
Common Patterns
# Resource naming conventions
GET /users # List all users
GET /users/{id} # Get specific user
POST /users # Create new user
PUT /users/{id} # Replace user
PATCH /users/{id} # Update user fields
DELETE /users/{id} # Delete user
# Nested resources
GET /users/{id}/orders # Get user's orders
POST /users/{id}/orders # Create order for user
# Filtering, sorting, pagination
GET /users?status=active&sort=name&page=2&limit=20
# Search
GET /users/search?q=john
# Bulk operations
POST /users/bulk # Create multiple users
DELETE /users/bulk?ids=1,2,3 # Delete multiple users
Examples
RESTful Response Structure
{
"data": {
"id": "123",
"type": "user",
"attributes": {
"name": "John Smith",
"email": "john@example.com",
"created_at": "2024-01-15T10:30:00Z"
},
"relationships": {
"orders": {
"links": {
"related": "/users/123/orders"
}
}
}
},
"links": {
"self": "/users/123"
},
"meta": {
"request_id": "abc-123"
}
}
Pagination Response
{
"data": [...],
"meta": {
"total": 100,
"page": 2,
"per_page": 20,
"total_pages": 5
},
"links": {
"self": "/users?page=2",
"first": "/users?page=1",
"prev": "/users?page=1",
"next": "/users?page=3",
"last": "/users?page=5"
}
}
HTTP Status Codes
# Success
200 OK # Request succeeded
201 Created # Resource created
204 No Content # Success, no body (DELETE)
# Redirection
301 Moved Permanently
304 Not Modified # Cached response valid
# Client Errors
400 Bad Request # Invalid syntax
401 Unauthorised # Authentication required
403 Forbidden # Authorisation denied
404 Not Found # Resource not found
409 Conflict # Resource conflict
422 Unprocessable # Validation errors
429 Too Many Requests
# Server Errors
500 Internal Error # Server error
502 Bad Gateway # Upstream error
503 Service Unavailable
504 Gateway Timeout
GraphQL Principles
Key Concepts
| Concept | Description |
|---|---|
| Schema-First | Strongly typed schema defines API contract |
| Single Endpoint | All operations through one URL |
| Client-Specified Queries | Clients request exactly what they need |
| Introspection | Schema is self-documenting |
| Resolvers | Functions that fetch data for fields |
| Subscriptions | Real-time updates via WebSockets |
Common Patterns
# Schema Definition
type User {
id: ID!
name: String!
email: String!
orders: [Order!]!
createdAt: DateTime!
}
type Order {
id: ID!
total: Float!
status: OrderStatus!
user: User!
}
enum OrderStatus {
PENDING
PROCESSING
SHIPPED
DELIVERED
}
type Query {
user(id: ID!): User
users(filter: UserFilter, limit: Int, offset: Int): [User!]!
}
type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
deleteUser(id: ID!): Boolean!
}
type Subscription {
orderStatusChanged(userId: ID!): Order!
}
input CreateUserInput {
name: String!
email: String!
}
input UserFilter {
status: String
createdAfter: DateTime
}
Examples
Query Examples
# Basic query
query GetUser {
user(id: "123") {
name
email
}
}
# Query with arguments and nested data
query GetUserWithOrders($userId: ID!) {
user(id: $userId) {
name
email
orders {
id
total
status
}
}
}
# Multiple queries in one request
query DashboardData {
currentUser {
name
notifications {
unreadCount
}
}
recentOrders(limit: 5) {
id
total
}
}
# Fragments for reusable fields
fragment UserBasicInfo on User {
id
name
email
}
query GetUsers {
users {
...UserBasicInfo
createdAt
}
}
Mutation Examples
# Create mutation
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
email
}
}
# Variables
{
"input": {
"name": "Jane Doe",
"email": "jane@example.com"
}
}
# Update mutation with optimistic response
mutation UpdateUserStatus($id: ID!, $status: String!) {
updateUser(id: $id, input: { status: $status }) {
id
status
updatedAt
}
}
Error Handling
{
"data": {
"user": null
},
"errors": [
{
"message": "User not found",
"locations": [{ "line": 2, "column": 3 }],
"path": ["user"],
"extensions": {
"code": "NOT_FOUND",
"timestamp": "2024-01-15T10:30:00Z"
}
}
]
}
gRPC Principles
Key Concepts
| Concept | Description |
|---|---|
| Protocol Buffers | Binary serialisation format for messages |
| HTTP/2 | Multiplexed streams, header compression |
| Strongly Typed | Code generation from .proto files |
| Bi-directional Streaming | Client and server can stream data |
| Deadlines | Built-in timeout propagation |
| Interceptors | Middleware for cross-cutting concerns |
Communication Patterns
graph LR
subgraph "gRPC Communication Types"
subgraph "Unary"
C1[Client] -->|Request| S1[Server]
S1 -->|Response| C1
end
subgraph "Server Streaming"
C2[Client] -->|Request| S2[Server]
S2 -->|Stream| C2
end
subgraph "Client Streaming"
C3[Client] -->|Stream| S3[Server]
S3 -->|Response| C3
end
subgraph "Bi-directional"
C4[Client] <-->|Stream| S4[Server]
end
end
Common Patterns
// user_service.proto
syntax = "proto3";
package userservice;
option go_package = "github.com/example/user-service";
import "google/protobuf/timestamp.proto";
import "google/protobuf/empty.proto";
service UserService {
// Unary RPC
rpc GetUser(GetUserRequest) returns (User);
rpc CreateUser(CreateUserRequest) returns (User);
rpc UpdateUser(UpdateUserRequest) returns (User);
rpc DeleteUser(DeleteUserRequest) returns (google.protobuf.Empty);
// Server streaming
rpc ListUsers(ListUsersRequest) returns (stream User);
// Client streaming
rpc BulkCreateUsers(stream CreateUserRequest) returns (BulkCreateResponse);
// Bi-directional streaming
rpc Chat(stream ChatMessage) returns (stream ChatMessage);
}
message User {
string id = 1;
string name = 2;
string email = 3;
UserStatus status = 4;
google.protobuf.Timestamp created_at = 5;
}
enum UserStatus {
USER_STATUS_UNSPECIFIED = 0;
USER_STATUS_ACTIVE = 1;
USER_STATUS_INACTIVE = 2;
}
message GetUserRequest {
string id = 1;
}
message CreateUserRequest {
string name = 1;
string email = 2;
}
message ListUsersRequest {
int32 page_size = 1;
string page_token = 2;
string filter = 3;
}
message BulkCreateResponse {
int32 created_count = 1;
repeated string failed_ids = 2;
}
Examples
Go gRPC Server
package main
import (
"context"
"log"
"net"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
pb "github.com/example/user-service/proto"
)
type server struct {
pb.UnimplementedUserServiceServer
}
func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
// Check for deadline
if ctx.Err() == context.DeadlineExceeded {
return nil, status.Error(codes.DeadlineExceeded, "deadline exceeded")
}
user, err := findUser(req.Id)
if err != nil {
return nil, status.Error(codes.NotFound, "user not found")
}
return user, nil
}
func (s *server) ListUsers(req *pb.ListUsersRequest, stream pb.UserService_ListUsersServer) error {
users := getUsers(req.Filter)
for _, user := range users {
if err := stream.Send(user); err != nil {
return err
}
}
return nil
}
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterUserServiceServer(s, &server{})
log.Printf("server listening at %v", lis.Addr())
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
Go gRPC Client
package main
import (
"context"
"log"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
pb "github.com/example/user-service/proto"
)
func main() {
conn, err := grpc.Dial("localhost:50051",
grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("failed to connect: %v", err)
}
defer conn.Close()
client := pb.NewUserServiceClient(conn)
// Set deadline
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
// Unary call
user, err := client.GetUser(ctx, &pb.GetUserRequest{Id: "123"})
if err != nil {
log.Fatalf("could not get user: %v", err)
}
log.Printf("User: %s", user.Name)
// Server streaming
stream, err := client.ListUsers(ctx, &pb.ListUsersRequest{PageSize: 10})
if err != nil {
log.Fatalf("could not list users: %v", err)
}
for {
user, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
log.Fatalf("error receiving: %v", err)
}
log.Printf("Received user: %s", user.Name)
}
}
Versioning Strategies
Key Concepts
| Strategy | Location | Example |
|---|---|---|
| URI Path | URL path | /v1/users, /v2/users |
| Query Parameter | Query string | /users?version=1 |
| Header | Custom header | X-API-Version: 1 |
| Accept Header | Media type | Accept: application/vnd.api+json;version=1 |
| Subdomain | Host | v1.api.example.com |
Common Patterns
# URI versioning (most common)
GET /api/v1/users
GET /api/v2/users
# Header versioning
GET /api/users
X-API-Version: 2
# Accept header versioning
GET /api/users
Accept: application/vnd.example.v2+json
# Query parameter versioning
GET /api/users?version=2
Examples
Version Negotiation Middleware (Node.js)
const express = require('express');
const app = express();
// Version extraction middleware
const extractVersion = (req, res, next) => {
// Check URI path
const pathMatch = req.path.match(/^\/v(\d+)\//);
if (pathMatch) {
req.apiVersion = parseInt(pathMatch[1]);
return next();
}
// Check header
const headerVersion = req.get('X-API-Version');
if (headerVersion) {
req.apiVersion = parseInt(headerVersion);
return next();
}
// Check Accept header
const acceptHeader = req.get('Accept');
const acceptMatch = acceptHeader?.match(/version=(\d+)/);
if (acceptMatch) {
req.apiVersion = parseInt(acceptMatch[1]);
return next();
}
// Default version
req.apiVersion = 1;
next();
};
app.use(extractVersion);
// Version-specific routes
app.get('/users', (req, res) => {
if (req.apiVersion === 1) {
return res.json({ users: getUsersV1() });
}
if (req.apiVersion === 2) {
return res.json({ data: getUsersV2(), meta: {} });
}
res.status(400).json({ error: 'Unsupported API version' });
});
Deprecation Headers
// Add deprecation warnings
app.use('/v1', (req, res, next) => {
res.set('Deprecation', 'true');
res.set('Sunset', 'Sat, 31 Dec 2024 23:59:59 GMT');
res.set('Link', '</v2/docs>; rel="successor-version"');
next();
});
Semantic Versioning for APIs
# API changelog
versions:
- version: "2.0.0"
released: "2024-01-15"
changes:
- type: breaking
description: "Changed user response format"
- type: feature
description: "Added bulk operations"
- version: "1.1.0"
released: "2023-10-01"
changes:
- type: feature
description: "Added filtering options"
- type: deprecation
description: "Deprecated /users/all endpoint"
Authentication and Authorisation
Key Concepts
| Method | Use Case | Token Location |
|---|---|---|
| API Keys | Server-to-server, simple auth | Header or query |
| Basic Auth | Simple username/password | Authorization header |
| JWT | Stateless, self-contained tokens | Authorization header |
| OAuth 2.0 | Delegated authorisation | Authorization header |
| mTLS | High security, service mesh | TLS certificate |
OAuth 2.0 Flow
sequenceDiagram
participant User
participant Client
participant AuthServer as Authorisation Server
participant API as Resource Server
User->>Client: 1. Initiate login
Client->>AuthServer: 2. Authorisation request
AuthServer->>User: 3. Login prompt
User->>AuthServer: 4. Authenticate
AuthServer->>Client: 5. Authorisation code
Client->>AuthServer: 6. Exchange code for tokens
AuthServer->>Client: 7. Access token + Refresh token
Client->>API: 8. API request with access token
API->>Client: 9. Protected resource
Common Patterns
# API Key authentication
curl -H "X-API-Key: your-api-key" https://api.example.com/users
# Basic authentication
curl -u username:password https://api.example.com/users
# Equivalent to:
curl -H "Authorization: Basic base64(username:password)" https://api.example.com/users
# Bearer token (JWT/OAuth)
curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." https://api.example.com/users
Examples
JWT Structure
// JWT consists of three parts: header.payload.signature
// Header
{
"alg": "RS256",
"typ": "JWT",
"kid": "key-id-123"
}
// Payload
{
"iss": "https://auth.example.com", // Issuer
"sub": "user-123", // Subject
"aud": "https://api.example.com", // Audience
"exp": 1705312200, // Expiration
"iat": 1705308600, // Issued at
"nbf": 1705308600, // Not before
"jti": "unique-token-id", // JWT ID
"scope": "read:users write:users", // Scopes
"roles": ["admin", "user"] // Custom claims
}
// Signature
RSASHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
privateKey
)
JWT Validation Middleware (Node.js)
const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');
const client = jwksClient({
jwksUri: 'https://auth.example.com/.well-known/jwks.json',
cache: true,
rateLimit: true
});
const getKey = (header, callback) => {
client.getSigningKey(header.kid, (err, key) => {
if (err) return callback(err);
callback(null, key.getPublicKey());
});
};
const authMiddleware = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing token' });
}
const token = authHeader.substring(7);
jwt.verify(token, getKey, {
algorithms: ['RS256'],
issuer: 'https://auth.example.com',
audience: 'https://api.example.com'
}, (err, decoded) => {
if (err) {
return res.status(401).json({ error: 'Invalid token' });
}
req.user = decoded;
next();
});
};
// Scope-based authorisation
const requireScope = (...requiredScopes) => {
return (req, res, next) => {
const tokenScopes = req.user.scope?.split(' ') || [];
const hasScope = requiredScopes.every(scope =>
tokenScopes.includes(scope)
);
if (!hasScope) {
return res.status(403).json({
error: 'Insufficient scope',
required: requiredScopes,
provided: tokenScopes
});
}
next();
};
};
// Usage
app.get('/users',
authMiddleware,
requireScope('read:users'),
getUsers
);
app.post('/users',
authMiddleware,
requireScope('write:users'),
createUser
);
OAuth 2.0 Token Exchange
// Authorisation Code Exchange
const axios = require('axios');
async function exchangeCodeForTokens(code) {
const response = await axios.post('https://auth.example.com/oauth/token', {
grant_type: 'authorization_code',
code: code,
client_id: process.env.CLIENT_ID,
client_secret: process.env.CLIENT_SECRET,
redirect_uri: 'https://app.example.com/callback'
});
return {
accessToken: response.data.access_token,
refreshToken: response.data.refresh_token,
expiresIn: response.data.expires_in
};
}
// Refresh Token
async function refreshAccessToken(refreshToken) {
const response = await axios.post('https://auth.example.com/oauth/token', {
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: process.env.CLIENT_ID,
client_secret: process.env.CLIENT_SECRET
});
return response.data.access_token;
}
Rate Limiting and Throttling
Key Concepts
| Algorithm | Description | Use Case |
|---|---|---|
| Fixed Window | Count requests per time window | Simple implementation |
| Sliding Window | Rolling time window | Smoother limiting |
| Token Bucket | Tokens refill at fixed rate | Allow bursts |
| Leaky Bucket | Process at constant rate | Steady output |
Rate Limiting Flow
flowchart TD
A[Request] --> B{Check Rate Limit}
B -->|Under Limit| C[Increment Counter]
C --> D[Process Request]
D --> E[Return Response with Headers]
B -->|Over Limit| F[Return 429 Too Many Requests]
F --> G[Include Retry-After Header]
Common Patterns
# Rate limit response headers
HTTP/1.1 200 OK
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 999
X-RateLimit-Reset: 1705312200
X-RateLimit-Policy: 1000;w=3600
# Rate limited response
HTTP/1.1 429 Too Many Requests
Retry-After: 3600
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1705312200
{
"error": "rate_limit_exceeded",
"message": "Too many requests",
"retry_after": 3600
}
Examples
Token Bucket Implementation (Node.js)
class TokenBucket {
constructor(capacity, refillRate) {
this.capacity = capacity;
this.tokens = capacity;
this.refillRate = refillRate; // tokens per second
this.lastRefill = Date.now();
}
refill() {
const now = Date.now();
const elapsed = (now - this.lastRefill) / 1000;
const tokensToAdd = elapsed * this.refillRate;
this.tokens = Math.min(this.capacity, this.tokens + tokensToAdd);
this.lastRefill = now;
}
consume(tokens = 1) {
this.refill();
if (this.tokens >= tokens) {
this.tokens -= tokens;
return true;
}
return false;
}
getWaitTime(tokens = 1) {
if (this.tokens >= tokens) return 0;
return ((tokens - this.tokens) / this.refillRate) * 1000;
}
}
// Rate limiting middleware
const buckets = new Map();
const rateLimiter = (options = {}) => {
const {
capacity = 100,
refillRate = 10,
keyGenerator = (req) => req.ip
} = options;
return (req, res, next) => {
const key = keyGenerator(req);
if (!buckets.has(key)) {
buckets.set(key, new TokenBucket(capacity, refillRate));
}
const bucket = buckets.get(key);
if (bucket.consume()) {
res.set('X-RateLimit-Limit', capacity);
res.set('X-RateLimit-Remaining', Math.floor(bucket.tokens));
res.set('X-RateLimit-Reset', Math.ceil(Date.now() / 1000 + capacity / refillRate));
next();
} else {
const retryAfter = Math.ceil(bucket.getWaitTime() / 1000);
res.set('Retry-After', retryAfter);
res.status(429).json({
error: 'rate_limit_exceeded',
retry_after: retryAfter
});
}
};
};
// Usage with different limits
app.use('/api/public', rateLimiter({ capacity: 100, refillRate: 10 }));
app.use('/api/premium', rateLimiter({ capacity: 1000, refillRate: 100 }));
Redis-Based Rate Limiting
const Redis = require('ioredis');
const redis = new Redis();
async function slidingWindowRateLimit(key, limit, windowSeconds) {
const now = Date.now();
const windowStart = now - (windowSeconds * 1000);
const multi = redis.multi();
// Remove old entries
multi.zremrangebyscore(key, 0, windowStart);
// Add current request
multi.zadd(key, now, `${now}-${Math.random()}`);
// Count requests in window
multi.zcard(key);
// Set expiry
multi.expire(key, windowSeconds);
const results = await multi.exec();
const requestCount = results[2][1];
return {
allowed: requestCount <= limit,
remaining: Math.max(0, limit - requestCount),
resetAt: Math.ceil((now + windowSeconds * 1000) / 1000)
};
}
// Middleware
const redisRateLimiter = (limit, windowSeconds) => {
return async (req, res, next) => {
const key = `ratelimit:${req.ip}`;
const result = await slidingWindowRateLimit(key, limit, windowSeconds);
res.set('X-RateLimit-Limit', limit);
res.set('X-RateLimit-Remaining', result.remaining);
res.set('X-RateLimit-Reset', result.resetAt);
if (result.allowed) {
next();
} else {
res.status(429).json({ error: 'Rate limit exceeded' });
}
};
};
Documentation Best Practices (OpenAPI/Swagger)
Key Concepts
| Concept | Description |
|---|---|
| OpenAPI Specification | Standard format for describing REST APIs |
| Schema Objects | Define data models with JSON Schema |
| Security Schemes | Document authentication methods |
| Examples | Provide sample requests and responses |
| Tags | Group related operations |
| Callbacks | Document webhook patterns |
Common Patterns
# openapi.yaml
openapi: 3.1.0
info:
title: User Management API
description: |
API for managing users in the system.
## Authentication
All endpoints require Bearer token authentication.
## Rate Limits
- Standard: 1000 requests/hour
- Premium: 10000 requests/hour
version: 2.0.0
contact:
name: API Support
email: api-support@example.com
url: https://support.example.com
license:
name: MIT
url: https://opensource.org/licenses/MIT
servers:
- url: https://api.example.com/v2
description: Production
- url: https://staging-api.example.com/v2
description: Staging
- url: http://localhost:3000/v2
description: Local development
tags:
- name: Users
description: User management operations
- name: Orders
description: Order management operations
security:
- bearerAuth: []
paths:
/users:
get:
tags:
- Users
summary: List users
description: Retrieve a paginated list of users with optional filtering.
operationId: listUsers
parameters:
- name: status
in: query
description: Filter by user status
schema:
type: string
enum: [active, inactive, pending]
- name: page
in: query
description: Page number
schema:
type: integer
minimum: 1
default: 1
- name: limit
in: query
description: Items per page
schema:
type: integer
minimum: 1
maximum: 100
default: 20
responses:
'200':
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/UserListResponse'
examples:
default:
$ref: '#/components/examples/UserListExample'
headers:
X-RateLimit-Limit:
$ref: '#/components/headers/RateLimitLimit'
X-RateLimit-Remaining:
$ref: '#/components/headers/RateLimitRemaining'
'401':
$ref: '#/components/responses/Unauthorised'
'429':
$ref: '#/components/responses/RateLimited'
post:
tags:
- Users
summary: Create user
description: Create a new user account.
operationId: createUser
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateUserRequest'
examples:
basic:
summary: Basic user creation
value:
name: John Smith
email: john@example.com
withMetadata:
summary: User with metadata
value:
name: Jane Doe
email: jane@example.com
metadata:
department: Engineering
responses:
'201':
description: User created
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'400':
$ref: '#/components/responses/ValidationError'
'409':
description: Email already exists
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/users/{userId}:
parameters:
- name: userId
in: path
required: true
description: User ID
schema:
type: string
format: uuid
get:
tags:
- Users
summary: Get user by ID
operationId: getUser
responses:
'200':
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'404':
$ref: '#/components/responses/NotFound'
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: JWT token obtained from /auth/token
apiKey:
type: apiKey
in: header
name: X-API-Key
schemas:
User:
type: object
required:
- id
- name
- email
- status
- createdAt
properties:
id:
type: string
format: uuid
description: Unique user identifier
example: 123e4567-e89b-12d3-a456-426614174000
name:
type: string
minLength: 1
maxLength: 100
description: User's full name
example: John Smith
email:
type: string
format: email
description: User's email address
example: john@example.com
status:
type: string
enum: [active, inactive, pending]
description: Account status
example: active
createdAt:
type: string
format: date-time
description: Account creation timestamp
example: '2024-01-15T10:30:00Z'
metadata:
type: object
additionalProperties: true
description: Custom metadata
CreateUserRequest:
type: object
required:
- name
- email
properties:
name:
type: string
minLength: 1
maxLength: 100
email:
type: string
format: email
metadata:
type: object
additionalProperties: true
UserListResponse:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/User'
meta:
$ref: '#/components/schemas/PaginationMeta'
links:
$ref: '#/components/schemas/PaginationLinks'
PaginationMeta:
type: object
properties:
total:
type: integer
page:
type: integer
perPage:
type: integer
totalPages:
type: integer
PaginationLinks:
type: object
properties:
self:
type: string
format: uri
first:
type: string
format: uri
prev:
type: string
format: uri
nullable: true
next:
type: string
format: uri
nullable: true
last:
type: string
format: uri
Error:
type: object
required:
- code
- message
properties:
code:
type: string
message:
type: string
details:
type: array
items:
type: object
properties:
field:
type: string
message:
type: string
responses:
Unauthorised:
description: Authentication required
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
code: UNAUTHORISED
message: Authentication required
NotFound:
description: Resource not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
code: NOT_FOUND
message: Resource not found
ValidationError:
description: Validation failed
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
code: VALIDATION_ERROR
message: Validation failed
details:
- field: email
message: Invalid email format
RateLimited:
description: Rate limit exceeded
headers:
Retry-After:
schema:
type: integer
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
code: RATE_LIMITED
message: Too many requests
headers:
RateLimitLimit:
schema:
type: integer
description: Request limit per hour
RateLimitRemaining:
schema:
type: integer
description: Remaining requests in current window
examples:
UserListExample:
value:
data:
- id: 123e4567-e89b-12d3-a456-426614174000
name: John Smith
email: john@example.com
status: active
createdAt: '2024-01-15T10:30:00Z'
meta:
total: 100
page: 1
perPage: 20
totalPages: 5
links:
self: /users?page=1
first: /users?page=1
next: /users?page=2
last: /users?page=5
Documentation Generation Commands
# Generate documentation from OpenAPI spec
npx @redocly/cli build-docs openapi.yaml -o docs/index.html
# Validate OpenAPI specification
npx @redocly/cli lint openapi.yaml
# Generate client SDK
npx openapi-generator-cli generate -i openapi.yaml -g typescript-axios -o ./client
# Start Swagger UI locally
docker run -p 8080:8080 -e SWAGGER_JSON=/spec/openapi.yaml \
-v $(pwd):/spec swaggerapi/swagger-ui
# Generate server stubs
npx openapi-generator-cli generate -i openapi.yaml -g nodejs-express-server -o ./server
Quick Reference
HTTP Status Codes
| Code | Meaning | When to Use |
|---|---|---|
200 |
OK | Successful GET, PUT, PATCH |
201 |
Created | Successful POST |
204 |
No Content | Successful DELETE |
400 |
Bad Request | Malformed syntax |
401 |
Unauthorised | Missing/invalid authentication |
403 |
Forbidden | Valid auth, no permission |
404 |
Not Found | Resource doesn't exist |
409 |
Conflict | Resource conflict |
422 |
Unprocessable | Validation errors |
429 |
Too Many Requests | Rate limited |
500 |
Internal Error | Server error |
503 |
Service Unavailable | Maintenance/overload |
API Design Checklist
| Category | Best Practice |
|---|---|
| Naming | Use plural nouns for collections (/users not /user) |
| Naming | Use kebab-case for URLs (/user-profiles) |
| Naming | Use camelCase for JSON properties |
| Versioning | Always version your API (/v1/) |
| Errors | Return consistent error format with codes |
| Pagination | Use cursor-based for large datasets |
| Filtering | Support common query parameters |
| Security | Use HTTPS everywhere |
| Security | Implement rate limiting |
| Security | Validate all inputs |
| Documentation | Provide examples for all endpoints |
| Performance | Support compression (gzip) |
| Performance | Implement caching headers |
Common Headers
| Header | Purpose | Example |
|---|---|---|
Authorization |
Authentication | Bearer <token> |
Content-Type |
Request body format | application/json |
Accept |
Preferred response format | application/json |
X-Request-ID |
Request tracing | uuid-value |
X-RateLimit-* |
Rate limit info | X-RateLimit-Remaining: 99 |
Cache-Control |
Caching directives | max-age=3600 |
ETag |
Resource version | "abc123" |
If-None-Match |
Conditional request | "abc123" |
Common Issues and Solutions
Issue: N+1 Query Problem in REST APIs
Symptoms: Multiple requests to fetch related data, poor performance.
Solution: Use compound documents or sparse fieldsets.
# Instead of:
GET /users/1
GET /users/1/orders
GET /users/1/profile
# Use:
GET /users/1?include=orders,profile
# Or GraphQL which handles this naturally
Issue: Breaking Changes Affecting Clients
Symptoms: Client applications break after API updates.
Solution: Implement proper versioning and deprecation strategy.
// Add deprecation notices
app.use('/v1', (req, res, next) => {
res.set('Deprecation', 'true');
res.set('Sunset', 'Sat, 31 Dec 2024 23:59:59 GMT');
console.warn(`Deprecated endpoint called: ${req.path}`);
next();
});
// Use semantic versioning for changes
// MAJOR: Breaking changes
// MINOR: New features (backwards compatible)
// PATCH: Bug fixes
Issue: JWT Token Expiry During Long Operations
Symptoms: Operations fail midway due to expired tokens.
Solution: Implement token refresh mechanism and handle expiry gracefully.
// Client-side token refresh
async function apiRequest(url, options) {
let response = await fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${getAccessToken()}`
}
});
if (response.status === 401) {
await refreshAccessToken();
response = await fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${getAccessToken()}`
}
});
}
return response;
}
Issue: Inconsistent Error Responses
Symptoms: Clients struggle to handle errors due to varying formats.
Solution: Standardise error response format.
// Standard error handler
app.use((err, req, res, next) => {
const statusCode = err.statusCode || 500;
res.status(statusCode).json({
error: {
code: err.code || 'INTERNAL_ERROR',
message: err.message || 'An unexpected error occurred',
details: err.details || [],
requestId: req.id,
timestamp: new Date().toISOString(),
path: req.path
}
});
});
Issue: Over-fetching or Under-fetching Data
Symptoms: REST endpoints return too much or too little data.
Solution: Implement sparse fieldsets or consider GraphQL.
# Sparse fieldsets for REST
GET /users?fields=id,name,email
# Expand related resources
GET /users?fields=id,name&expand=orders(id,total)
// Implementation
app.get('/users', (req, res) => {
const fields = req.query.fields?.split(',') || null;
const users = getUsers();
if (fields) {
const filtered = users.map(user =>
Object.fromEntries(
Object.entries(user).filter(([key]) => fields.includes(key))
)
);
return res.json(filtered);
}
res.json(users);
});
Issue: CORS Errors in Browser Clients
Symptoms: Browser blocks API requests with CORS errors.
Solution: Configure proper CORS headers.
const cors = require('cors');
app.use(cors({
origin: ['https://app.example.com', 'https://admin.example.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Request-ID'],
exposedHeaders: ['X-RateLimit-Remaining', 'X-RateLimit-Reset'],
credentials: true,
maxAge: 86400 // Cache preflight for 24 hours
}));
Issue: Rate Limiting Not Working with Load Balancers
Symptoms: Rate limits not enforced correctly across multiple instances.
Solution: Use distributed rate limiting with Redis.
// Use Redis for distributed state
const Redis = require('ioredis');
const redis = new Redis(process.env.REDIS_URL);
// Ensure consistent key generation
const getRateLimitKey = (req) => {
// Use user ID for authenticated requests
if (req.user) {
return `ratelimit:user:${req.user.id}`;
}
// Fall back to IP, considering proxy headers
const ip = req.headers['x-forwarded-for']?.split(',')[0] || req.ip;
return `ratelimit:ip:${ip}`;
};
Related Topics
The following topics would complement this API Design Patterns cheatsheet:
-
Microservices Architecture - Service decomposition, inter-service communication patterns, and distributed system design that builds on API design principles.
-
API Gateway Patterns - Kong, AWS API Gateway, or Traefik configuration for routing, load balancing, and API management at scale.
-
Event-Driven Architecture - Message queues, event sourcing, and CQRS patterns that complement synchronous API design.
-
API Security Best Practices - Advanced security topics including OWASP API Security Top 10, input validation, and secure coding practices.
-
Service Mesh (Istio/Linkerd) - Traffic management, observability, and security for microservices that consume APIs.
-
API Testing Strategies - Contract testing with Pact, load testing with k6, and integration testing patterns for APIs.