Difficulty: Intermediate
How do you design a production-ready REST API in Node.js? Explain middleware, error handling, and validation patterns.
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
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.
// 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).
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.
REST, Middleware, Error Handling, Validation, Authentication