All posts

REST API Design: The Complete Guide for Backend Developers

Description: A comprehensive guide to designing clean, scalable REST APIs — covering URL structure, authentication, versioning, validation, security, and testing with real-world Node.js examples.

April 22, 202622 min read
REST API Design: The Complete Guide for Backend Developers

REST API Design: The Complete Guide for Backend Developers

Poor API design costs companies thousands of hours in maintenance and frustrated developers. A well-designed API, on the other hand, becomes a competitive advantage that developers love to use.

Whether you're building internal microservices or public APIs used by millions, the principles of good API design remain the same. This guide will teach you how to design REST APIs that are intuitive, scalable, and maintainable.

By the end, you'll know how to structure endpoints, handle errors gracefully, implement authentication, version your APIs, and follow industry best practices used by companies like Stripe, GitHub, and Twilio.

Prerequisites

To get the most from this guide, you should have:

  • Basic understanding of HTTP (GET, POST, status codes)
  • Experience building backend services in any language
  • Familiarity with JSON
  • No advanced API knowledge required

What Makes a Good API?

A well-designed API is:

Intuitive: Developers can predict how it works without reading extensive documentation

Consistent: Similar operations follow similar patterns

Flexible: Supports current needs and future extensions

Secure: Protects data and prevents abuse

Well-Documented: Clear examples and explanations

Versioned: Changes don't break existing clients

REST Fundamentals

REST (Representational State Transfer) is an architectural style for designing networked applications. It relies on stateless, client-server communication using HTTP.

Core Principles

1. Resources Over Actions

Think in terms of resources (nouns), not actions (verbs).

❌ Bad:
POST /api/createUser
POST /api/getUser
POST /api/deleteUser

✅ Good:
POST   /api/users      (create user)
GET    /api/users/:id  (get user)
DELETE /api/users/:id  (delete user)

2. Use HTTP Methods Correctly

GET     - Retrieve resource(s)
POST    - Create new resource
PUT     - Replace entire resource
PATCH   - Update part of resource
DELETE  - Remove resource

3. Stateless Communication

Each request contains all information needed. Server doesn't store client state.

// ❌ Bad: Server-side sessions
GET /api/cart
Cookie: sessionId=abc123

// ✅ Good: Token in header
GET /api/users/123/cart
Authorization: Bearer eyJhbGc...

4. Use Standard HTTP Status Codes

2xx - Success
  200 OK              - Request successful
  201 Created         - Resource created
  204 No Content      - Success, no response body

3xx - Redirection
  301 Moved Permanently
  304 Not Modified

4xx - Client Errors
  400 Bad Request     - Invalid request
  401 Unauthorized    - Authentication required
  403 Forbidden       - Authenticated but not allowed
  404 Not Found       - Resource doesn't exist
  429 Too Many Requests

5xx - Server Errors
  500 Internal Server Error
  503 Service Unavailable

URL Structure and Naming

Use Nouns for Resources

✅ Good:
/users
/products
/orders
/posts
/comments

❌ Bad:
/getUsers
/createProduct
/updateOrder

Plural vs Singular

Use plural for collections:

✅ Preferred:
GET /users          (all users)
GET /users/123      (specific user)

❌ Inconsistent:
GET /user           (all users?)
GET /user/123       (specific user)

Hierarchical Relationships

Show relationships through URL structure:

GET  /users/123/posts           (all posts by user 123)
GET  /users/123/posts/456       (post 456 by user 123)
GET  /posts/456/comments        (comments on post 456)
POST /posts/456/comments        (add comment to post 456)

Use Hyphens, Not Underscores

✅ Good:
/api/user-profiles
/api/order-history

❌ Bad:
/api/user_profiles
/api/order_history

Keep URLs Lowercase

✅ Good:
/api/products/electronics

❌ Bad:
/api/Products/Electronics
/api/PRODUCTS/ELECTRONICS

Endpoint Design Patterns

CRUD Operations

Standard pattern for Create, Read, Update, Delete:

// Express.js example

// List all users (with pagination)
app.get('/api/users', async (req, res) => {
  const { page = 1, limit = 20 } = req.query;
  
  const users = await User.find()
    .limit(limit)
    .skip((page - 1) * limit);
  
  const total = await User.countDocuments();
  
  res.json({
    data: users,
    pagination: {
      page: parseInt(page),
      limit: parseInt(limit),
      total,
      pages: Math.ceil(total / limit)
    }
  });
});

// Get single user
app.get('/api/users/:id', async (req, res) => {
  const user = await User.findById(req.params.id);
  
  if (!user) {
    return res.status(404).json({
      error: {
        code: 'USER_NOT_FOUND',
        message: 'User not found'
      }
    });
  }
  
  res.json({ data: user });
});

// Create user
app.post('/api/users', async (req, res) => {
  const { name, email, password } = req.body;
  
  // Validation
  if (!email || !password) {
    return res.status(400).json({
      error: {
        code: 'VALIDATION_ERROR',
        message: 'Email and password required',
        fields: {
          email: !email ? 'Required' : undefined,
          password: !password ? 'Required' : undefined
        }
      }
    });
  }
  
  const user = await User.create({ name, email, password });
  
  res.status(201).json({ data: user });
});

// Update user (full replacement)
app.put('/api/users/:id', async (req, res) => {
  const user = await User.findByIdAndUpdate(
    req.params.id,
    req.body,
    { new: true, overwrite: true }
  );
  
  if (!user) {
    return res.status(404).json({
      error: {
        code: 'USER_NOT_FOUND',
        message: 'User not found'
      }
    });
  }
  
  res.json({ data: user });
});

// Update user (partial update)
app.patch('/api/users/:id', async (req, res) => {
  const user = await User.findByIdAndUpdate(
    req.params.id,
    { $set: req.body },
    { new: true }
  );
  
  if (!user) {
    return res.status(404).json({
      error: {
        code: 'USER_NOT_FOUND',
        message: 'User not found'
      }
    });
  }
  
  res.json({ data: user });
});

// Delete user
app.delete('/api/users/:id', async (req, res) => {
  const user = await User.findByIdAndDelete(req.params.id);
  
  if (!user) {
    return res.status(404).json({
      error: {
        code: 'USER_NOT_FOUND',
        message: 'User not found'
      }
    });
  }
  
  res.status(204).send();
});

Filtering and Searching

// Filtering with query parameters
app.get('/api/products', async (req, res) => {
  const { 
    category, 
    minPrice, 
    maxPrice, 
    inStock,
    search 
  } = req.query;
  
  const query = {};
  
  if (category) {
    query.category = category;
  }
  
  if (minPrice || maxPrice) {
    query.price = {};
    if (minPrice) query.price.$gte = parseFloat(minPrice);
    if (maxPrice) query.price.$lte = parseFloat(maxPrice);
  }
  
  if (inStock !== undefined) {
    query.inStock = inStock === 'true';
  }
  
  if (search) {
    query.$text = { $search: search };
  }
  
  const products = await Product.find(query);
  
  res.json({ data: products });
});

// Usage:
// GET /api/products?category=electronics&minPrice=100&maxPrice=500&inStock=true
// GET /api/products?search=laptop

Sorting

app.get('/api/products', async (req, res) => {
  const { sortBy = 'createdAt', order = 'desc' } = req.query;
  
  const sortOrder = order === 'asc' ? 1 : -1;
  const sortOptions = { [sortBy]: sortOrder };
  
  const products = await Product.find().sort(sortOptions);
  
  res.json({ data: products });
});

// Usage:
// GET /api/products?sortBy=price&order=asc
// GET /api/products?sortBy=name&order=desc

Pagination

// Offset-based pagination
app.get('/api/posts', async (req, res) => {
  const page = parseInt(req.query.page) || 1;
  const limit = parseInt(req.query.limit) || 20;
  const skip = (page - 1) * limit;
  
  const posts = await Post.find()
    .limit(limit)
    .skip(skip)
    .sort({ createdAt: -1 });
  
  const total = await Post.countDocuments();
  
  res.json({
    data: posts,
    meta: {
      page,
      limit,
      total,
      totalPages: Math.ceil(total / limit),
      hasNext: page * limit < total,
      hasPrev: page > 1
    }
  });
});

// Cursor-based pagination (better for large datasets)
app.get('/api/posts', async (req, res) => {
  const { cursor, limit = 20 } = req.query;
  
  const query = cursor ? { _id: { $lt: cursor } } : {};
  
  const posts = await Post.find(query)
    .limit(parseInt(limit) + 1)
    .sort({ _id: -1 });
  
  const hasNext = posts.length > limit;
  const data = hasNext ? posts.slice(0, -1) : posts;
  const nextCursor = hasNext ? posts[posts.length - 2]._id : null;
  
  res.json({
    data,
    meta: {
      nextCursor,
      hasNext
    }
  });
});

// Usage:
// GET /api/posts?limit=20
// GET /api/posts?cursor=507f1f77bcf86cd799439011&limit=20

Field Selection

Allow clients to request specific fields:

app.get('/api/users/:id', async (req, res) => {
  const { fields } = req.query;
  
  let query = User.findById(req.params.id);
  
  if (fields) {
    // fields=name,email,createdAt
    const selectedFields = fields.split(',').join(' ');
    query = query.select(selectedFields);
  }
  
  const user = await query;
  
  res.json({ data: user });
});

// Usage:
// GET /api/users/123?fields=name,email

Nested Resources

// Get user's orders
app.get('/api/users/:userId/orders', async (req, res) => {
  const orders = await Order.find({ userId: req.params.userId });
  res.json({ data: orders });
});

// Get specific order for user
app.get('/api/users/:userId/orders/:orderId', async (req, res) => {
  const order = await Order.findOne({
    _id: req.params.orderId,
    userId: req.params.userId
  });
  
  if (!order) {
    return res.status(404).json({
      error: {
        code: 'ORDER_NOT_FOUND',
        message: 'Order not found'
      }
    });
  }
  
  res.json({ data: order });
});

// Create order for user
app.post('/api/users/:userId/orders', async (req, res) => {
  const order = await Order.create({
    userId: req.params.userId,
    ...req.body
  });
  
  res.status(201).json({ data: order });
});

Response Format Standards

Consistent Response Structure

Use a consistent envelope for all responses:

// Success response
{
  "data": {
    "id": "123",
    "name": "John Doe",
    "email": "[email protected]"
  }
}

// List response
{
  "data": [
    { "id": "1", "name": "Item 1" },
    { "id": "2", "name": "Item 2" }
  ],
  "meta": {
    "page": 1,
    "limit": 20,
    "total": 50
  }
}

// Error response
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid email format",
    "details": {
      "field": "email",
      "value": "invalid-email"
    }
  }
}

Standardized Error Format

class APIError extends Error {
  constructor(statusCode, code, message, details = null) {
    super(message);
    this.statusCode = statusCode;
    this.code = code;
    this.details = details;
  }
}

// Error handler middleware
app.use((err, req, res, next) => {
  if (err instanceof APIError) {
    return res.status(err.statusCode).json({
      error: {
        code: err.code,
        message: err.message,
        details: err.details
      }
    });
  }
  
  // Unexpected errors
  console.error(err);
  res.status(500).json({
    error: {
      code: 'INTERNAL_SERVER_ERROR',
      message: 'An unexpected error occurred'
    }
  });
});

// Usage
app.post('/api/users', async (req, res, next) => {
  try {
    const { email } = req.body;
    
    if (!isValidEmail(email)) {
      throw new APIError(
        400,
        'VALIDATION_ERROR',
        'Invalid email format',
        { field: 'email', value: email }
      );
    }
    
    const existingUser = await User.findOne({ email });
    if (existingUser) {
      throw new APIError(
        409,
        'USER_EXISTS',
        'User with this email already exists'
      );
    }
    
    const user = await User.create(req.body);
    res.status(201).json({ data: user });
  } catch (err) {
    next(err);
  }
});

Include Metadata

// Pagination metadata
{
  "data": [...],
  "meta": {
    "page": 2,
    "limit": 20,
    "total": 156,
    "totalPages": 8
  }
}

// Timestamp metadata
{
  "data": {...},
  "meta": {
    "requestId": "req_abc123",
    "timestamp": "2026-04-22T10:30:00Z",
    "version": "v1"
  }
}

// Links for navigation (HATEOAS)
{
  "data": [...],
  "links": {
    "self": "/api/users?page=2",
    "first": "/api/users?page=1",
    "prev": "/api/users?page=1",
    "next": "/api/users?page=3",
    "last": "/api/users?page=8"
  }
}

Authentication and Authorization

API Keys

Simple but less secure. Good for server-to-server communication.

const API_KEYS = {
  'key_abc123': { name: 'Mobile App', permissions: ['read', 'write'] },
  'key_xyz789': { name: 'Analytics', permissions: ['read'] }
};

function authenticateAPIKey(req, res, next) {
  const apiKey = req.headers['x-api-key'];
  
  if (!apiKey || !API_KEYS[apiKey]) {
    return res.status(401).json({
      error: {
        code: 'INVALID_API_KEY',
        message: 'Invalid or missing API key'
      }
    });
  }
  
  req.apiClient = API_KEYS[apiKey];
  next();
}

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

JWT (JSON Web Tokens)

Industry standard for stateless authentication.

const jwt = require('jsonwebtoken');

// Generate token
app.post('/api/auth/login', async (req, res) => {
  const { email, password } = req.body;
  
  const user = await User.findOne({ email });
  if (!user || !(await user.comparePassword(password))) {
    return res.status(401).json({
      error: {
        code: 'INVALID_CREDENTIALS',
        message: 'Invalid email or password'
      }
    });
  }
  
  const token = jwt.sign(
    { 
      userId: user._id,
      email: user.email,
      role: user.role
    },
    process.env.JWT_SECRET,
    { expiresIn: '7d' }
  );
  
  res.json({
    data: {
      token,
      user: {
        id: user._id,
        name: user.name,
        email: user.email
      }
    }
  });
});

// Verify token middleware
function authenticateJWT(req, res, next) {
  const authHeader = req.headers.authorization;
  
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({
      error: {
        code: 'MISSING_TOKEN',
        message: 'Authorization token required'
      }
    });
  }
  
  const token = authHeader.substring(7);
  
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded;
    next();
  } catch (err) {
    return res.status(401).json({
      error: {
        code: 'INVALID_TOKEN',
        message: 'Invalid or expired token'
      }
    });
  }
}

// Protected route
app.get('/api/profile', authenticateJWT, async (req, res) => {
  const user = await User.findById(req.user.userId);
  res.json({ data: user });
});

Role-Based Access Control

function authorize(...allowedRoles) {
  return (req, res, next) => {
    if (!req.user) {
      return res.status(401).json({
        error: {
          code: 'UNAUTHORIZED',
          message: 'Authentication required'
        }
      });
    }
    
    if (!allowedRoles.includes(req.user.role)) {
      return res.status(403).json({
        error: {
          code: 'FORBIDDEN',
          message: 'Insufficient permissions'
        }
      });
    }
    
    next();
  };
}

// Usage
app.delete(
  '/api/users/:id',
  authenticateJWT,
  authorize('admin', 'moderator'),
  async (req, res) => {
    // Only admins and moderators can delete users
    await User.findByIdAndDelete(req.params.id);
    res.status(204).send();
  }
);

OAuth 2.0

For third-party integrations:

const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;

passport.use(new GoogleStrategy({
    clientID: process.env.GOOGLE_CLIENT_ID,
    clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    callbackURL: '/api/auth/google/callback'
  },
  async (accessToken, refreshToken, profile, done) => {
    let user = await User.findOne({ googleId: profile.id });
    
    if (!user) {
      user = await User.create({
        googleId: profile.id,
        email: profile.emails[0].value,
        name: profile.displayName
      });
    }
    
    done(null, user);
  }
));

app.get('/api/auth/google',
  passport.authenticate('google', { scope: ['profile', 'email'] })
);

app.get('/api/auth/google/callback',
  passport.authenticate('google', { session: false }),
  (req, res) => {
    const token = jwt.sign(
      { userId: req.user._id },
      process.env.JWT_SECRET,
      { expiresIn: '7d' }
    );
    
    res.redirect(`/auth/success?token=${token}`);
  }
);

Rate Limiting

Protect your API from abuse:

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

// Global rate limit
const globalLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // 100 requests per window
  message: {
    error: {
      code: 'RATE_LIMIT_EXCEEDED',
      message: 'Too many requests, please try again later'
    }
  },
  standardHeaders: true, // Return rate limit info in headers
  legacyHeaders: false
});

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

// Endpoint-specific rate limit
const loginLimiter = rateLimit({
  windowMs: 60 * 60 * 1000, // 1 hour
  max: 5, // 5 attempts per hour
  skipSuccessfulRequests: true
});

app.post('/api/auth/login', loginLimiter, async (req, res) => {
  // Login logic
});

// Custom rate limiter with Redis
const Redis = require('ioredis');
const redis = new Redis();

async function rateLimitMiddleware(req, res, next) {
  const key = `rate_limit:${req.ip}`;
  const limit = 100;
  const window = 60; // seconds
  
  const current = await redis.incr(key);
  
  if (current === 1) {
    await redis.expire(key, window);
  }
  
  if (current > limit) {
    return res.status(429).json({
      error: {
        code: 'RATE_LIMIT_EXCEEDED',
        message: 'Too many requests'
      }
    });
  }
  
  // Add rate limit headers
  res.setHeader('X-RateLimit-Limit', limit);
  res.setHeader('X-RateLimit-Remaining', limit - current);
  
  next();
}

Versioning Strategies

URL Versioning (Most Common)

// Version in URL path
app.use('/api/v1', v1Routes);
app.use('/api/v2', v2Routes);

// v1/users.js
router.get('/users/:id', (req, res) => {
  // Old implementation
  res.json({
    id: user.id,
    name: user.name
  });
});

// v2/users.js
router.get('/users/:id', (req, res) => {
  // New implementation with more fields
  res.json({
    id: user.id,
    firstName: user.firstName,
    lastName: user.lastName,
    fullName: `${user.firstName} ${user.lastName}`
  });
});

Header Versioning

function versionMiddleware(req, res, next) {
  const version = req.headers['api-version'] || '1';
  req.apiVersion = version;
  next();
}

app.use(versionMiddleware);

app.get('/api/users/:id', (req, res) => {
  if (req.apiVersion === '2') {
    // v2 response
    res.json({ firstName: user.firstName, lastName: user.lastName });
  } else {
    // v1 response
    res.json({ name: user.name });
  }
});

Content Negotiation

app.get('/api/users/:id', (req, res) => {
  const accept = req.headers['accept'];
  
  if (accept.includes('application/vnd.myapi.v2+json')) {
    // v2 response
    res.json({ firstName: user.firstName, lastName: user.lastName });
  } else {
    // v1 response
    res.json({ name: user.name });
  }
});

Request Validation

Using Joi

const Joi = require('joi');

const createUserSchema = Joi.object({
  name: Joi.string().min(2).max(50).required(),
  email: Joi.string().email().required(),
  password: Joi.string().min(8).required(),
  age: Joi.number().integer().min(18).max(120).optional(),
  role: Joi.string().valid('user', 'admin').default('user')
});

function validateRequest(schema) {
  return (req, res, next) => {
    const { error, value } = schema.validate(req.body, {
      abortEarly: false, // Return all errors
      stripUnknown: true // Remove unknown fields
    });
    
    if (error) {
      const errors = error.details.reduce((acc, err) => {
        acc[err.path[0]] = err.message;
        return acc;
      }, {});
      
      return res.status(400).json({
        error: {
          code: 'VALIDATION_ERROR',
          message: 'Request validation failed',
          fields: errors
        }
      });
    }
    
    req.body = value;
    next();
  };
}

app.post('/api/users', validateRequest(createUserSchema), async (req, res) => {
  const user = await User.create(req.body);
  res.status(201).json({ data: user });
});

Using Express Validator

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

app.post('/api/users',
  [
    body('email').isEmail().normalizeEmail(),
    body('password').isLength({ min: 8 }),
    body('name').trim().notEmpty(),
    body('age').optional().isInt({ min: 18, max: 120 })
  ],
  (req, res) => {
    const errors = validationResult(req);
    
    if (!errors.isEmpty()) {
      return res.status(400).json({
        error: {
          code: 'VALIDATION_ERROR',
          message: 'Request validation failed',
          fields: errors.mapped()
        }
      });
    }
    
    // Proceed with user creation
  }
);

Documentation

OpenAPI/Swagger

const swaggerJsdoc = require('swagger-jsdoc');
const swaggerUi = require('swagger-ui-express');

const options = {
  definition: {
    openapi: '3.0.0',
    info: {
      title: 'My API',
      version: '1.0.0',
      description: 'A sample API'
    },
    servers: [
      {
        url: 'http://localhost:3000',
        description: 'Development server'
      }
    ]
  },
  apis: ['./routes/*.js']
};

const specs = swaggerJsdoc(options);
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs));

/**
 * @swagger
 * /api/users:
 *   get:
 *     summary: Retrieve a list of users
 *     tags: [Users]
 *     parameters:
 *       - in: query
 *         name: page
 *         schema:
 *           type: integer
 *         description: Page number
 *       - in: query
 *         name: limit
 *         schema:
 *           type: integer
 *         description: Number of items per page
 *     responses:
 *       200:
 *         description: A list of users
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 data:
 *                   type: array
 *                   items:
 *                     $ref: '#/components/schemas/User'
 *                 meta:
 *                   type: object
 */
app.get('/api/users', async (req, res) => {
  // Implementation
});

/**
 * @swagger
 * components:
 *   schemas:
 *     User:
 *       type: object
 *       required:
 *         - name
 *         - email
 *       properties:
 *         id:
 *           type: string
 *         name:
 *           type: string
 *         email:
 *           type: string
 *           format: email
 *         createdAt:
 *           type: string
 *           format: date-time
 */

Performance Optimization

Caching

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

function cache(duration) {
  return async (req, res, next) => {
    const key = `cache:${req.originalUrl}`;
    
    try {
      const cached = await client.get(key);
      
      if (cached) {
        return res.json(JSON.parse(cached));
      }
      
      // Store original send
      res.originalSend = res.send;
      
      // Override send
      res.send = function(data) {
        res.originalSend(data);
        client.setex(key, duration, data);
      };
      
      next();
    } catch (err) {
      next();
    }
  };
}

// Usage
app.get('/api/products', cache(300), async (req, res) => {
  const products = await Product.find();
  res.json({ data: products });
});

Compression

const compression = require('compression');

app.use(compression({
  filter: (req, res) => {
    if (req.headers['x-no-compression']) {
      return false;
    }
    return compression.filter(req, res);
  },
  level: 6 // Compression level (0-9)
}));

Database Query Optimization

// Bad: N+1 query problem
app.get('/api/posts', async (req, res) => {
  const posts = await Post.find();
  
  for (let post of posts) {
    post.author = await User.findById(post.authorId); // N queries!
  }
  
  res.json({ data: posts });
});

// Good: Use populate/join
app.get('/api/posts', async (req, res) => {
  const posts = await Post.find().populate('author'); // 1 query
  res.json({ data: posts });
});

// Good: Use projection to select only needed fields
app.get('/api/users', async (req, res) => {
  const users = await User.find()
    .select('name email -_id') // Only name and email, exclude _id
    .lean(); // Return plain objects, not Mongoose documents
  
  res.json({ data: users });
});

Security Best Practices

Input Sanitization

const mongoSanitize = require('express-mongo-sanitize');
const xss = require('xss-clean');

// Prevent NoSQL injection
app.use(mongoSanitize());

// Prevent XSS attacks
app.use(xss());

// Helmet for security headers
const helmet = require('helmet');
app.use(helmet());

CORS Configuration

const cors = require('cors');

app.use(cors({
  origin: process.env.ALLOWED_ORIGINS.split(','),
  methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true,
  maxAge: 86400 // 24 hours
}));

SQL Injection Prevention

// ❌ Bad: Vulnerable to SQL injection
app.get('/api/users', (req, res) => {
  const { name } = req.query;
  const query = `SELECT * FROM users WHERE name = '${name}'`;
  db.query(query, (err, results) => {
    res.json({ data: results });
  });
});

// ✅ Good: Use parameterized queries
app.get('/api/users', (req, res) => {
  const { name } = req.query;
  const query = 'SELECT * FROM users WHERE name = ?';
  db.query(query, [name], (err, results) => {
    res.json({ data: results });
  });
});

Testing APIs

Unit Tests

const request = require('supertest');
const app = require('../app');

describe('User API', () => {
  describe('GET /api/users/:id', () => {
    it('should return user when ID is valid', async () => {
      const res = await request(app)
        .get('/api/users/123')
        .expect(200);
      
      expect(res.body.data).toHaveProperty('id', '123');
      expect(res.body.data).toHaveProperty('name');
    });
    
    it('should return 404 when user not found', async () => {
      const res = await request(app)
        .get('/api/users/999')
        .expect(404);
      
      expect(res.body.error.code).toBe('USER_NOT_FOUND');
    });
  });
  
  describe('POST /api/users', () => {
    it('should create user with valid data', async () => {
      const userData = {
        name: 'John Doe',
        email: '[email protected]',
        password: 'password123'
      };
      
      const res = await request(app)
        .post('/api/users')
        .send(userData)
        .expect(201);
      
      expect(res.body.data).toHaveProperty('id');
      expect(res.body.data.email).toBe(userData.email);
    });
    
    it('should return 400 with invalid email', async () => {
      const userData = {
        name: 'John Doe',
        email: 'invalid-email',
        password: 'password123'
      };
      
      const res = await request(app)
        .post('/api/users')
        .send(userData)
        .expect(400);
      
      expect(res.body.error.code).toBe('VALIDATION_ERROR');
    });
  });
});

Integration Tests

describe('Order Flow Integration', () => {
  let authToken;
  let userId;
  let productId;
  
  beforeAll(async () => {
    // Create test user and login
    const user = await request(app)
      .post('/api/users')
      .send({
        name: 'Test User',
        email: '[email protected]',
        password: 'password123'
      });
    
    userId = user.body.data.id;
    
    const login = await request(app)
      .post('/api/auth/login')
      .send({
        email: '[email protected]',
        password: 'password123'
      });
    
    authToken = login.body.data.token;
    
    // Create test product
    const product = await request(app)
      .post('/api/products')
      .set('Authorization', `Bearer ${authToken}`)
      .send({
        name: 'Test Product',
        price: 99.99
      });
    
    productId = product.body.data.id;
  });
  
  it('should complete full order flow', async () => {
    // Add to cart
    await request(app)
      .post(`/api/users/${userId}/cart`)
      .set('Authorization', `Bearer ${authToken}`)
      .send({ productId, quantity: 2 })
      .expect(201);
    
    // Create order
    const order = await request(app)
      .post('/api/orders')
      .set('Authorization', `Bearer ${authToken}`)
      .send({
        items: [{ productId, quantity: 2 }],
        shippingAddress: '123 Main St'
      })
      .expect(201);
    
    expect(order.body.data).toHaveProperty('id');
    expect(order.body.data.total).toBe(199.98);
    
    // Verify order was created
    const getOrder = await request(app)
      .get(`/api/orders/${order.body.data.id}`)
      .set('Authorization', `Bearer ${authToken}`)
      .expect(200);
    
    expect(getOrder.body.data.status).toBe('pending');
  });
});

Monitoring and Logging

const winston = require('winston');
const morgan = require('morgan');

// Configure logger
const logger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.json()
  ),
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' })
  ]
});

if (process.env.NODE_ENV !== 'production') {
  logger.add(new winston.transports.Console({
    format: winston.format.simple()
  }));
}

// HTTP request logging
app.use(morgan('combined', {
  stream: {
    write: (message) => logger.info(message.trim())
  }
}));

// Request ID middleware
app.use((req, res, next) => {
  req.id = require('uuid').v4();
  res.setHeader('X-Request-ID', req.id);
  next();
});

// Log all API requests
app.use((req, res, next) => {
  const start = Date.now();
  
  res.on('finish', () => {
    logger.info({
      requestId: req.id,
      method: req.method,
      url: req.url,
      statusCode: res.statusCode,
      duration: Date.now() - start,
      userAgent: req.headers['user-agent']
    });
  });
  
  next();
});

// Error logging
app.use((err, req, res, next) => {
  logger.error({
    requestId: req.id,
    error: {
      message: err.message,
      stack: err.stack
    },
    request: {
      method: req.method,
      url: req.url,
      headers: req.headers,
      body: req.body
    }
  });
  
  next(err);
});

Real-World Example: E-commerce API

Complete implementation:

const express = require('express');
const mongoose = require('mongoose');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');

const app = express();
app.use(express.json());

// Models
const UserSchema = new mongoose.Schema({
  email: { type: String, required: true, unique: true },
  password: { type: String, required: true },
  role: { type: String, enum: ['user', 'admin'], default: 'user' }
});

const ProductSchema = new mongoose.Schema({
  name: { type: String, required: true },
  price: { type: Number, required: true },
  stock: { type: Number, default: 0 },
  category: String,
  description: String
});

const OrderSchema = new mongoose.Schema({
  userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
  items: [{
    productId: { type: mongoose.Schema.Types.ObjectId, ref: 'Product' },
    quantity: Number,
    price: Number
  }],
  total: Number,
  status: {
    type: String,
    enum: ['pending', 'processing', 'shipped', 'delivered'],
    default: 'pending'
  },
  createdAt: { type: Date, default: Date.now }
});

const User = mongoose.model('User', UserSchema);
const Product = mongoose.model('Product', ProductSchema);
const Order = mongoose.model('Order', OrderSchema);

// Auth endpoints
app.post('/api/auth/register', async (req, res) => {
  try {
    const { email, password } = req.body;
    
    const hashedPassword = await bcrypt.hash(password, 10);
    const user = await User.create({
      email,
      password: hashedPassword
    });
    
    const token = jwt.sign(
      { userId: user._id, role: user.role },
      process.env.JWT_SECRET,
      { expiresIn: '7d' }
    );
    
    res.status(201).json({
      data: {
        token,
        user: { id: user._id, email: user.email }
      }
    });
  } catch (err) {
    res.status(400).json({
      error: { code: 'REGISTRATION_FAILED', message: err.message }
    });
  }
});

// Product endpoints
app.get('/api/products', async (req, res) => {
  const { page = 1, limit = 20, category, minPrice, maxPrice } = req.query;
  
  const query = {};
  if (category) query.category = category;
  if (minPrice || maxPrice) {
    query.price = {};
    if (minPrice) query.price.$gte = parseFloat(minPrice);
    if (maxPrice) query.price.$lte = parseFloat(maxPrice);
  }
  
  const products = await Product.find(query)
    .limit(limit)
    .skip((page - 1) * limit);
  
  const total = await Product.countDocuments(query);
  
  res.json({
    data: products,
    meta: {
      page: parseInt(page),
      limit: parseInt(limit),
      total,
      pages: Math.ceil(total / limit)
    }
  });
});

app.post('/api/products', authenticateJWT, authorize('admin'), async (req, res) => {
  const product = await Product.create(req.body);
  res.status(201).json({ data: product });
});

// Order endpoints
app.post('/api/orders', authenticateJWT, async (req, res) => {
  const { items } = req.body;
  
  // Calculate total
  let total = 0;
  const orderItems = [];
  
  for (const item of items) {
    const product = await Product.findById(item.productId);
    if (!product || product.stock < item.quantity) {
      return res.status(400).json({
        error: {
          code: 'INSUFFICIENT_STOCK',
          message: `Insufficient stock for product ${product.name}`
        }
      });
    }
    
    orderItems.push({
      productId: product._id,
      quantity: item.quantity,
      price: product.price
    });
    
    total += product.price * item.quantity;
    
    // Reduce stock
    product.stock -= item.quantity;
    await product.save();
  }
  
  const order = await Order.create({
    userId: req.user.userId,
    items: orderItems,
    total
  });
  
  res.status(201).json({ data: order });
});

app.get('/api/orders', authenticateJWT, async (req, res) => {
  const orders = await Order.find({ userId: req.user.userId })
    .populate('items.productId', 'name price')
    .sort({ createdAt: -1 });
  
  res.json({ data: orders });
});

app.listen(3000, () => console.log('Server running on port 3000'));

Conclusion

Designing great REST APIs is both an art and a science. The principles in this guide—intuitive URLs, consistent responses, proper error handling, security, and documentation—form the foundation of APIs that developers love to use.

Remember that API design is iterative. Start with the basics, get feedback from users, and continuously improve. The best APIs are shaped by real-world use cases and developer experience.

Quick Checklist

Before releasing your API, verify:

  • URLs follow RESTful conventions
  • HTTP methods used correctly
  • Consistent response format
  • Comprehensive error handling
  • Authentication and authorization implemented
  • Rate limiting in place
  • Input validation on all endpoints
  • API versioning strategy
  • Documentation complete
  • Tests cover critical paths
  • Logging and monitoring configured
  • Security headers set
  • CORS configured properly

Resources

Tools:

  • Postman - API testing
  • Swagger/OpenAPI - Documentation
  • Insomnia - API client
  • Newman - Automated testing

Libraries:

  • Express.js - Node.js framework
  • Fastify - Fast Node.js framework
  • Django REST - Python framework
  • Spring Boot - Java framework

Books:

  • "RESTful Web APIs" by Leonard Richardson
  • "API Design Patterns" by JJ Geewax

Start building better APIs today. Your developers will thank you.