# API Design Patterns Concrete patterns for REST and GraphQL API design with examples. ## Patterns Index 1. [REST vs GraphQL Decision](#1-rest-vs-graphql-decision) 2. [Resource Naming Conventions](#2-resource-naming-conventions) 3. [API Versioning Strategies](#3-api-versioning-strategies) 4. [Error Handling Patterns](#4-error-handling-patterns) 5. [Pagination Patterns](#5-pagination-patterns) 6. [Authentication Patterns](#6-authentication-patterns) 7. [Rate Limiting Design](#7-rate-limiting-design) 8. [Idempotency Patterns](#8-idempotency-patterns) --- ## 1. REST vs GraphQL Decision ### When to Use REST | Scenario | Why REST | |----------|----------| | Simple CRUD operations | Less complexity, widely understood | | Public APIs | Better caching, easier documentation | | File uploads/downloads | Native HTTP support | | Microservices communication | Simpler service-to-service calls | | Caching is critical | HTTP caching built-in | ### When to Use GraphQL | Scenario | Why GraphQL | |----------|-------------| | Mobile apps with bandwidth constraints | Request only needed fields | | Complex nested data | Single request for related data | | Rapidly changing frontend requirements | Frontend-driven queries | | Multiple client types | Each client queries what it needs | | Real-time subscriptions needed | Built-in subscription support | ### Hybrid Approach ``` ┌─────────────────────────────────────────────────────┐ │ API Gateway │ ├─────────────────────────────────────────────────────┤ │ /api/v1/* → REST (Public API, webhooks) │ │ /graphql → GraphQL (Mobile apps, dashboards) │ │ /files/* → REST (File uploads/downloads) │ └─────────────────────────────────────────────────────┘ ``` --- ## 2. Resource Naming Conventions ### REST Endpoint Patterns ``` # Collections (plural nouns) GET /users # List users POST /users # Create user GET /users/{id} # Get user PUT /users/{id} # Replace user PATCH /users/{id} # Update user DELETE /users/{id} # Delete user # Nested resources GET /users/{id}/orders # User's orders POST /users/{id}/orders # Create order for user GET /users/{id}/orders/{orderId} # Specific order # Actions (when CRUD doesn't fit) POST /users/{id}/activate # Activate user POST /orders/{id}/cancel # Cancel order POST /payments/{id}/refund # Refund payment # Filtering, sorting, pagination GET /users?status=active&sort=-created_at&limit=20&offset=40 GET /orders?user_id=123&status=pending ``` ### Naming Rules | Rule | Good | Bad | |------|------|-----| | Use plural nouns | `/users` | `/user` | | Use lowercase | `/user-profiles` | `/userProfiles` | | Use hyphens | `/order-items` | `/order_items` | | No verbs in URLs | `POST /orders` | `POST /createOrder` | | No file extensions | `/users/123` | `/users/123.json` | --- ## 3. API Versioning Strategies ### Strategy Comparison | Strategy | Example | Pros | Cons | |----------|---------|------|------| | URL Path | `/api/v1/users` | Explicit, easy routing | URL changes | | Header | `Accept: application/vnd.api+json;version=1` | Clean URLs | Hidden version | | Query Param | `/users?version=1` | Easy to test | Pollutes query string | ### Recommended: URL Path Versioning ```typescript // Express routing import v1Routes from './routes/v1'; import v2Routes from './routes/v2'; app.use('/api/v1', v1Routes); app.use('/api/v2', v2Routes); ``` ### Deprecation Strategy ```typescript // Add deprecation headers app.use('/api/v1', (req, res, next) => { res.set('Deprecation', 'true'); res.set('Sunset', 'Sat, 01 Jun 2025 00:00:00 GMT'); res.set('Link', '; rel="successor-version"'); next(); }, v1Routes); ``` ### Breaking vs Non-Breaking Changes **Non-breaking (safe):** - Adding new endpoints - Adding optional fields - Adding new enum values at end **Breaking (requires new version):** - Removing endpoints or fields - Renaming fields - Changing field types - Changing required/optional status --- ## 4. Error Handling Patterns ### Standard Error Response Format ```json { "error": { "code": "VALIDATION_ERROR", "message": "Request validation failed", "details": [ { "field": "email", "code": "INVALID_FORMAT", "message": "Must be a valid email address" }, { "field": "age", "code": "OUT_OF_RANGE", "message": "Must be between 18 and 120" } ], "documentation_url": "https://api.example.com/docs/errors#validation" }, "meta": { "request_id": "req_abc123", "timestamp": "2024-01-15T10:30:00Z" } } ``` ### Error Codes by Category ```typescript // Client errors (4xx) const ClientErrors = { VALIDATION_ERROR: 400, INVALID_JSON: 400, AUTHENTICATION_REQUIRED: 401, INVALID_TOKEN: 401, TOKEN_EXPIRED: 401, PERMISSION_DENIED: 403, RESOURCE_NOT_FOUND: 404, METHOD_NOT_ALLOWED: 405, CONFLICT: 409, RATE_LIMIT_EXCEEDED: 429, }; // Server errors (5xx) const ServerErrors = { INTERNAL_ERROR: 500, DATABASE_ERROR: 500, EXTERNAL_SERVICE_ERROR: 502, SERVICE_UNAVAILABLE: 503, }; ``` ### Error Handler Implementation ```typescript // Express error handler interface ApiError extends Error { code: string; statusCode: number; details?: Array<{ field: string; message: string }>; } const errorHandler: ErrorRequestHandler = (err: ApiError, req, res, next) => { const statusCode = err.statusCode || 500; const code = err.code || 'INTERNAL_ERROR'; // Log server errors if (statusCode >= 500) { logger.error({ err, requestId: req.id }, 'Server error'); } res.status(statusCode).json({ error: { code, message: statusCode >= 500 ? 'An unexpected error occurred' : err.message, details: err.details, ...(process.env.NODE_ENV === 'development' && { stack: err.stack }), }, meta: { request_id: req.id, timestamp: new Date().toISOString(), }, }); }; ``` --- ## 5. Pagination Patterns ### Offset-Based Pagination ``` GET /users?limit=20&offset=40 Response: { "data": [...], "pagination": { "total": 1250, "limit": 20, "offset": 40, "has_more": true } } ``` **Pros:** Simple, supports random access **Cons:** Inconsistent with concurrent inserts/deletes ### Cursor-Based Pagination ``` GET /users?limit=20&cursor=eyJpZCI6MTIzfQ== Response: { "data": [...], "pagination": { "limit": 20, "next_cursor": "eyJpZCI6MTQzfQ==", "prev_cursor": "eyJpZCI6MTIzfQ==", "has_more": true } } ``` **Pros:** Consistent with real-time data, efficient **Cons:** No random access, cursor encoding required ### Implementation Example ```typescript // Cursor-based pagination interface CursorPagination { limit: number; cursor?: string; direction?: 'forward' | 'backward'; } async function paginatedQuery( query: QueryBuilder, { limit, cursor, direction = 'forward' }: CursorPagination ): Promise<{ data: T[]; nextCursor?: string; hasMore: boolean }> { // Decode cursor const decoded = cursor ? JSON.parse(Buffer.from(cursor, 'base64').toString()) : null; // Apply cursor condition if (decoded) { query = direction === 'forward' ? query.where('id', '>', decoded.id) : query.where('id', '<', decoded.id); } // Fetch one extra to check if more exist const results = await query.limit(limit + 1).orderBy('id', direction === 'forward' ? 'asc' : 'desc'); const hasMore = results.length > limit; const data = hasMore ? results.slice(0, -1) : results; // Encode next cursor const nextCursor = hasMore ? Buffer.from(JSON.stringify({ id: data[data.length - 1].id })).toString('base64') : undefined; return { data, nextCursor, hasMore }; } ``` --- ## 6. Authentication Patterns ### JWT Authentication Flow ``` ┌──────────┐ 1. Login ┌──────────┐ │ Client │ ──────────────────▶ │ Server │ └──────────┘ └──────────┘ │ 2. Return JWT │ ◀──────────────────────────────────────── {access_token, refresh_token} │ │ 3. API Request │ ───────────────────────────────────────▶ Authorization: Bearer {token} │ │ 4. Validate & Respond │ ◀──────────────────────────────────────── ``` ### JWT Implementation ```typescript import jwt from 'jsonwebtoken'; interface TokenPayload { userId: string; email: string; roles: string[]; } // Generate tokens function generateTokens(user: User): { accessToken: string; refreshToken: string } { const payload: TokenPayload = { userId: user.id, email: user.email, roles: user.roles, }; const accessToken = jwt.sign(payload, process.env.JWT_SECRET!, { expiresIn: '15m', algorithm: 'RS256', }); const refreshToken = jwt.sign( { userId: user.id, tokenVersion: user.tokenVersion }, process.env.JWT_REFRESH_SECRET!, { expiresIn: '7d', algorithm: 'RS256' } ); return { accessToken, refreshToken }; } // Middleware const authenticate: RequestHandler = async (req, res, next) => { const authHeader = req.headers.authorization; if (!authHeader?.startsWith('Bearer ')) { return res.status(401).json({ error: { code: 'AUTHENTICATION_REQUIRED' } }); } try { const token = authHeader.slice(7); const payload = jwt.verify(token, process.env.JWT_SECRET!) as TokenPayload; req.user = payload; next(); } catch (err) { if (err instanceof jwt.TokenExpiredError) { return res.status(401).json({ error: { code: 'TOKEN_EXPIRED' } }); } return res.status(401).json({ error: { code: 'INVALID_TOKEN' } }); } }; ``` ### API Key Authentication (Service-to-Service) ```typescript // API key middleware const apiKeyAuth: RequestHandler = async (req, res, next) => { const apiKey = req.headers['x-api-key'] as string; if (!apiKey) { return res.status(401).json({ error: { code: 'API_KEY_REQUIRED' } }); } // Hash and lookup (never store plain API keys) const hashedKey = crypto.createHash('sha256').update(apiKey).digest('hex'); const client = await db.apiClients.findByHashedKey(hashedKey); if (!client || !client.isActive) { return res.status(401).json({ error: { code: 'INVALID_API_KEY' } }); } req.apiClient = client; next(); }; ``` --- ## 7. Rate Limiting Design ### Rate Limit Headers ``` HTTP/1.1 200 OK X-RateLimit-Limit: 100 X-RateLimit-Remaining: 95 X-RateLimit-Reset: 1705312800 Retry-After: 60 ``` ### Tiered Rate Limits ```typescript const rateLimits = { anonymous: { requests: 60, window: '1m' }, authenticated: { requests: 1000, window: '1h' }, premium: { requests: 10000, window: '1h' }, }; // Implementation with Redis import { RateLimiterRedis } from 'rate-limiter-flexible'; const createRateLimiter = (tier: keyof typeof rateLimits) => { const config = rateLimits[tier]; return new RateLimiterRedis({ storeClient: redisClient, keyPrefix: `ratelimit:${tier}`, points: config.requests, duration: parseDuration(config.window), }); }; ``` ### Rate Limit Response ```json { "error": { "code": "RATE_LIMIT_EXCEEDED", "message": "Too many requests", "details": { "limit": 100, "window": "1 minute", "retry_after": 45 } } } ``` --- ## 8. Idempotency Patterns ### Idempotency Key Header ``` POST /payments Idempotency-Key: payment_abc123_attempt1 Content-Type: application/json { "amount": 1000, "currency": "USD" } ``` ### Implementation ```typescript const idempotencyMiddleware: RequestHandler = async (req, res, next) => { const idempotencyKey = req.headers['idempotency-key'] as string; if (!idempotencyKey) { return next(); // Optional for some endpoints } // Check for existing response const cached = await redis.get(`idempotency:${idempotencyKey}`); if (cached) { const { statusCode, body } = JSON.parse(cached); return res.status(statusCode).json(body); } // Store response after processing const originalJson = res.json.bind(res); res.json = (body: any) => { redis.setex( `idempotency:${idempotencyKey}`, 86400, // 24 hours JSON.stringify({ statusCode: res.statusCode, body }) ); return originalJson(body); }; next(); }; ``` --- ## Quick Reference: HTTP Methods | Method | Idempotent | Safe | Cacheable | Request Body | |--------|------------|------|-----------|--------------| | GET | Yes | Yes | Yes | No | | HEAD | Yes | Yes | Yes | No | | POST | No | No | Conditional | Yes | | PUT | Yes | No | No | Yes | | PATCH | No | No | No | Yes | | DELETE | Yes | No | No | Optional | | OPTIONS | Yes | Yes | No | No |