REST API Design & Middleware

Difficulty: Intermediate

Question

How do you design a production-ready REST API in Node.js? Explain middleware, error handling, and validation patterns.

Answer

A production REST API needs: proper routing, input validation, authentication, error handling, logging, and rate limiting.

Middleware pattern: Functions that execute in order on each request. They can modify req/res, end the request, or call next() to pass control.

Key patterns: 1. Route → Validation middleware → Auth middleware → Controller → Service 2. Global error handler catches all errors 3. Zod/Joi for request validation 4. JWT for stateless authentication

Code examples

Express Middleware Chain

const express = require('express');
const app = express();

// 1. Global middleware
app.use(express.json()); // parse JSON body
app.use(cors());         // enable CORS
app.use(morgan('dev'));  // request logging

// 2. Route-specific middleware chain
app.post('/api/users',
  validateBody(createUserSchema),  // validation
  authenticate,                     // auth check
  authorize('admin'),               // role check
  userController.create             // handler
);

// 3. Middleware function anatomy
function authenticate(req, res, next) {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) return res.status(401).json({ error: 'No token' });

  try {
    req.user = jwt.verify(token, SECRET);
    next(); // pass to next middleware
  } catch {
    res.status(401).json({ error: 'Invalid token' });
  }
}

Each middleware does one thing. If it calls next(), the chain continues. If it sends a response, the chain stops.

Global Error Handler

// Custom error class
class AppError extends Error {
  constructor(message, statusCode) {
    super(message);
    this.statusCode = statusCode;
    this.isOperational = true;
  }
}

// In controllers/services - throw AppError
async function getUser(req, res, next) {
  try {
    const user = await User.findById(req.params.id);
    if (!user) throw new AppError('User not found', 404);
    res.json(user);
  } catch (err) {
    next(err); // pass to error handler
  }
}

// Global error handler (4 params = error middleware)
app.use((err, req, res, next) => {
  const status = err.statusCode || 500;
  const message = err.isOperational
    ? err.message
    : 'Internal server error';

  console.error(err.stack);
  res.status(status).json({ error: message });
});

Express recognizes error middleware by the 4 parameters (err, req, res, next). Always distinguish operational errors (expected) from programming errors (bugs).

Request Validation with Zod

const { z } = require('zod');

const createUserSchema = z.object({
  body: z.object({
    email: z.string().email('Invalid email'),
    password: z.string().min(8, 'Min 8 characters'),
    name: z.string().min(2).max(50),
    role: z.enum(['user', 'admin']).default('user'),
  }),
});

// Validation middleware factory
function validateBody(schema) {
  return (req, res, next) => {
    try {
      schema.parse({ body: req.body });
      next();
    } catch (err) {
      res.status(400).json({
        error: 'Validation failed',
        details: err.errors,
      });
    }
  };
}

Zod provides type-safe validation with clear error messages. The middleware factory pattern lets you reuse the validation logic for any schema.

Key points

Concepts covered

REST, Middleware, Error Handling, Validation, Authentication