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 /usersGET /users/123POST /usersPUT /users/123DELETE /users/123❌ Bad URLs:GET /getUsersPOST /createUserPUT /updateUserDELETE /deleteUser2. HTTP Methods Usage
typescript
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:
http
POST /api/usersContent-Type: application/json{ "name": "John Doe", "email": "john@example.com", "role": "user"}Response:
http
HTTP/1.1 201 CreatedContent-Type: application/jsonLocation: /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
typescript
interface APIError { status: number; code: string; message: string; details?: Record<string, any>;}// Example Implementationfunction 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
json
// 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
typescript
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 Implementationconst 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
typescript
interface PaginationParams { cursor?: string; limit: number;}interface PaginatedResponse<T> { items: T[]; nextCursor?: string; totalCount: number;}// Implementation Exampleasync 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
typescript
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
typescript
// Route Configurationconst router = express.Router();router.use('/v1', v1Routes);router.use('/v2', v2Routes);// Version-specific Implementationconst v1UserController = { getUser: (req: Request, res: Response) => { // V1 implementation }};const v2UserController = { getUser: (req: Request, res: Response) => { // V2 implementation with new features }};Documentation
OpenAPI Specification Example
yaml
openapi: 3.0.0info: title: User API version: 1.0.0paths: /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: integerBest 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