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 /deleteUser

2. 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: integer

Best Practices Checklist

  1. ✅ Use proper HTTP methods
  2. ✅ Implement consistent error handling
  3. ✅ Include proper authentication
  4. ✅ Implement rate limiting
  5. ✅ Use pagination for large datasets
  6. ✅ Version your API
  7. ✅ Provide comprehensive documentation
  8. ✅ Use proper status codes
  9. ✅ Implement CORS properly
  10. ✅ 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