RESTful API Design Principles
A comprehensive guide to designing robust and scalable RESTful APIs, covering best practices and common pitfalls.
RESTful API Design Principles
Introduction
Designing a well-structured RESTful API is crucial for building scalable and maintainable web applications. This guide covers essential principles, best practices, and common patterns in modern API design.
Core Principles
1. Resource-Oriented Design
APIs should be organized around resources:
✅ Good URLs:
GET /users
GET /users/123
POST /users
PUT /users/123
DELETE /users/123
❌ Bad URLs:
GET /getUsers
POST /createUser
PUT /updateUser
DELETE /deleteUser
2. HTTP Methods Usage
interface UserAPI { // GET: Retrieve resources getUsers(): Promise<User[]>; getUser(id: string): Promise<User>; // POST: Create resources createUser(data: UserInput): Promise<User>; // PUT: Update resources updateUser(id: string, data: UserInput): Promise<User>; // DELETE: Remove resources deleteUser(id: string): Promise<void>; // PATCH: Partial updates patchUser(id: string, data: Partial<UserInput>): Promise<User>; }
Request/Response Examples
User Creation
Request:
POST /api/users Content-Type: application/json { "name": "John Doe", "email": "john@example.com", "role": "user" }
Response:
HTTP/1.1 201 Created Content-Type: application/json Location: /api/users/123 { "id": "123", "name": "John Doe", "email": "john@example.com", "role": "user", "createdAt": "2024-01-20T10:00:00Z" }
Error Handling
Standard Error Response Format
interface APIError { status: number; code: string; message: string; details?: Record<string, any>; } // Example Implementation function createErrorResponse(error: APIError): Response { return new Response( JSON.stringify({ error: { code: error.code, message: error.message, details: error.details } }), { status: error.status, headers: { 'Content-Type': 'application/json' } } ); }
Common Error Scenarios
// 400 Bad Request { "error": { "code": "INVALID_INPUT", "message": "The request payload is invalid", "details": { "email": "Must be a valid email address", "name": "Cannot be empty" } } } // 401 Unauthorized { "error": { "code": "UNAUTHORIZED", "message": "Authentication required" } } // 403 Forbidden { "error": { "code": "FORBIDDEN", "message": "Insufficient permissions" } } // 404 Not Found { "error": { "code": "NOT_FOUND", "message": "Resource not found" } }
Authentication and Authorization
JWT Authentication Example
interface AuthService { async validateToken(token: string): Promise<User> { try { const decoded = jwt.verify(token, process.env.JWT_SECRET); return await db.users.findById(decoded.sub); } catch (error) { throw new UnauthorizedError('Invalid token'); } } } // Middleware Implementation const authMiddleware = async (req: Request, res: Response, next: NextFunction) => { const token = req.headers.authorization?.split(' ')[1]; if (!token) { return res.status(401).json({ error: { code: 'UNAUTHORIZED', message: 'No token provided' } }); } try { const user = await auth.validateToken(token); req.user = user; next(); } catch (error) { res.status(401).json({ error: { code: 'UNAUTHORIZED', message: 'Invalid token' } }); } };
Pagination and Filtering
Cursor-based Pagination
interface PaginationParams { cursor?: string; limit: number; } interface PaginatedResponse<T> { items: T[]; nextCursor?: string; totalCount: number; } // Implementation Example async function getPaginatedUsers(params: PaginationParams): Promise<PaginatedResponse<User>> { const { cursor, limit } = params; const query = cursor ? { createdAt: { $lt: new Date(cursor) } } : {}; const items = await db.users .find(query) .sort({ createdAt: -1 }) .limit(limit + 1) .toArray(); const hasMore = items.length > limit; if (hasMore) items.pop(); return { items, nextCursor: hasMore ? items[items.length - 1].createdAt.toISOString() : undefined, totalCount: await db.users.count(query) }; }
Rate Limiting
Implementation Example
interface RateLimitConfig { windowMs: number; // Time window in milliseconds max: number; // Max requests per window } class RateLimiter { private store: Map<string, number[]> = new Map(); constructor(private config: RateLimitConfig) {} isAllowed(key: string): boolean { const now = Date.now(); const windowStart = now - this.config.windowMs; // Get existing timestamps for this key const timestamps = this.store.get(key) || []; // Remove old timestamps const valid = timestamps.filter(t => t > windowStart); if (valid.length >= this.config.max) { return false; } // Add new timestamp valid.push(now); this.store.set(key, valid); return true; } }
API Versioning
URL-based Versioning
// Route Configuration const router = express.Router(); router.use('/v1', v1Routes); router.use('/v2', v2Routes); // Version-specific Implementation const v1UserController = { getUser: (req: Request, res: Response) => { // V1 implementation } }; const v2UserController = { getUser: (req: Request, res: Response) => { // V2 implementation with new features } };
Documentation
OpenAPI Specification Example
openapi: 3.0.0 info: title: User API version: 1.0.0 paths: /users: get: summary: Get users list parameters: - name: cursor in: query schema: type: string - name: limit in: query schema: type: integer default: 10 responses: '200': description: Successful response content: application/json: schema: type: object properties: items: type: array items: $ref: '#/components/schemas/User' nextCursor: type: string totalCount: type: integer
Best Practices Checklist
- ✅ Use proper HTTP methods
- ✅ Implement consistent error handling
- ✅ Include proper authentication
- ✅ Implement rate limiting
- ✅ Use pagination for large datasets
- ✅ Version your API
- ✅ Provide comprehensive documentation
- ✅ Use proper status codes
- ✅ Implement CORS properly
- ✅ Include proper security headers
Conclusion
A well-designed API follows consistent patterns, is well-documented, and provides a great developer experience. Remember to balance between following REST principles and practical considerations for your specific use case.
Created with WebInk - Demonstrating API documentation capabilities