Available for day contractsFrom 21st September I have availability for day and half day contracts. Please contact for more information.

Contact →
mikepreston.org

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.

API Architecture OverviewData LayerBackend ServicesAPI StylesAPI Gateway LayerDatabaseClient ApplicationsAPI GatewayAuthenticationRate LimitingCachingREST APIsGraphQL APIsgRPC APIsService AService BService CMessage QueueAPI Architecture OverviewData LayerBackend ServicesAPI StylesAPI Gateway LayerDatabaseClient ApplicationsAPI GatewayAuthenticationRate LimitingCachingREST APIsGraphQL APIsgRPC APIsService AService BService CMessage Queue

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

gRPC Communication TypesBi-directionalStreamClientServerClient StreamingStreamResponseClientServerServer StreamingRequestStreamClientServerUnaryRequestResponseClientServergRPC Communication TypesBi-directionalStreamClientServerClient StreamingStreamResponseClientServerServer StreamingRequestStreamClientServerUnaryRequestResponseClientServer

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

Resource ServerAuthorisation ServerClientUserResource ServerAuthorisation ServerClientUser1. Initiate login2. Authorisation request3. Login prompt4. Authenticate5. Authorisation code6. Exchange code for tokens7. Access token + Refresh token8. API request with access token9. Protected resourceResource ServerAuthorisation ServerClientUserResource ServerAuthorisation ServerClientUser1. Initiate login2. Authorisation request3. Login prompt4. Authenticate5. Authorisation code6. Exchange code for tokens7. Access token + Refresh token8. API request with access token9. 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

Under LimitOver LimitRequestCheck Rate LimitIncrement CounterProcess RequestReturn Response withHeadersReturn 429 Too ManyRequestsInclude Retry-AfterHeaderUnder LimitOver LimitRequestCheck Rate LimitIncrement CounterProcess RequestReturn Response withHeadersReturn 429 Too ManyRequestsInclude Retry-AfterHeader

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:

  1. Microservices Architecture - Service decomposition, inter-service communication patterns, and distributed system design that builds on API design principles.

  2. API Gateway Patterns - Kong, AWS API Gateway, or Traefik configuration for routing, load balancing, and API management at scale.

  3. Event-Driven Architecture - Message queues, event sourcing, and CQRS patterns that complement synchronous API design.

  4. API Security Best Practices - Advanced security topics including OWASP API Security Top 10, input validation, and secure coding practices.

  5. Service Mesh (Istio/Linkerd) - Traffic management, observability, and security for microservices that consume APIs.

  6. API Testing Strategies - Contract testing with Pact, load testing with k6, and integration testing patterns for APIs.