blog/7
Backend Development10 min read

API Design Best Practices for Modern Web Apps

By Marin Cholakov11/10/2024
API DesignRESTBackendNode.jsSecurity
API Design Best Practices for Modern Web Apps

Well-designed APIs are the backbone of modern web applications. Here are essential principles and practices for creating APIs that are both powerful and developer-friendly:

RESTful API Design Principles

1. Use Descriptive Resource Names

✅ Good
GET /api/users
GET /api/users/123
POST /api/users
PUT /api/users/123
DELETE /api/users/123

❌ Bad
GET /api/getUsers
GET /api/user?id=123
POST /api/createUser

2. HTTP Status Codes

Use appropriate status codes:

// Success responses
200 OK        // Successful GET, PUT, PATCH
201 Created   // Successful POST
204 No Content // Successful DELETE

// Client error responses
400 Bad Request      // Invalid request data
401 Unauthorized     // Authentication required
403 Forbidden        // Access denied
404 Not Found        // Resource doesn't exist
409 Conflict         // Resource conflict
422 Unprocessable    // Validation errors

// Server error responses
500 Internal Server Error
503 Service Unavailable

3. Consistent Error Response Format

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid input data",
    "details": [
      {
        "field": "email",
        "message": "Email is required"
      }
    ],
    "timestamp": "2024-11-15T10:30:00Z",
    "requestId": "req_123456"
  }
}

Pagination and Filtering

Cursor-based Pagination

// Request
GET /api/posts?limit=20&cursor=eyJpZCI6MTIzfQ

// Response
{
  "data": [...],
  "pagination": {
    "hasNextPage": true,
    "nextCursor": "eyJpZCI6MTQ0fQ",
    "limit": 20
  }
}

Filtering and Sorting

// Complex filtering
GET /api/products?category=electronics&price[gte]=100&price[lte]=500&sort=-createdAt&limit=10

// Response includes metadata
{
  "data": [...],
  "meta": {
    "total": 1250,
    "page": 1,
    "limit": 10,
    "filters": {
      "category": "electronics",
      "price": { "gte": 100, "lte": 500 }
    }
  }
}

Security Best Practices

1. Authentication & Authorization

// JWT-based authentication
const authenticateToken = (req, res, next) => {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];
  
  if (!token) {
    return res.status(401).json({ error: 'Access token required' });
  }
  
  jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
    if (err) return res.status(403).json({ error: 'Invalid token' });
    req.user = user;
    next();
  });
};

2. Rate Limiting

const rateLimit = require('express-rate-limit');

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // limit each IP to 100 requests per windowMs
  message: {
    error: 'Too many requests, please try again later'
  }
});

app.use('/api', limiter);

3. Input Validation

const { body, validationResult } = require('express-validator');

const validateUser = [
  body('email').isEmail().normalizeEmail(),
  body('password').isLength({ min: 8 }).matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/),
  body('name').trim().isLength({ min: 2, max: 50 }),
  
  (req, res, next) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(422).json({
        error: {
          code: 'VALIDATION_ERROR',
          message: 'Invalid input data',
          details: errors.array()
        }
      });
    }
    next();
  }
];

API Documentation

OpenAPI/Swagger Example

openapi: 3.0.0
info:
  title: User API
  version: 1.0.0
paths:
  /users:
    get:
      summary: Get users
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/User'

Performance Optimization

1. Caching Strategy

const redis = require('redis');
const client = redis.createClient();

const cacheMiddleware = (duration = 3600) => {
  return async (req, res, next) => {
    const key = `cache:${req.method}:${req.originalUrl}`;
    
    try {
      const cached = await client.get(key);
      if (cached) {
        return res.json(JSON.parse(cached));
      }
      
      // Store original res.json
      const originalJson = res.json;
      res.json = function(data) {
        // Cache the response
        client.setex(key, duration, JSON.stringify(data));
        originalJson.call(this, data);
      };
      
      next();
    } catch (error) {
      next();
    }
  };
};

2. Database Query Optimization

// Use database indexes
// SELECT with specific fields
const users = await db.users.findMany({
  select: {
    id: true,
    name: true,
    email: true,
    // Don't select password or sensitive data
  },
  where: {
    active: true
  },
  orderBy: {
    createdAt: 'desc'
  },
  take: 20
});

API Versioning

// URL versioning
GET /api/v1/users
GET /api/v2/users

// Header versioning
GET /api/users
Headers: {
  'Accept': 'application/vnd.api+json;version=2'
}

Following these practices will help you build APIs that are secure, performant, and maintainable.

Share this post