Building a Production-Ready REST API with Node.js and Express
April 28, 2025
Plenty of tutorials show a single index.js with every route crammed in. Here's the structure I actually reach for once an API needs to survive real traffic and a growing team.
Project Structure
src/
routes/
users.route.js
controllers/
users.controller.js
middleware/
errorHandler.js
validate.js
app.js
server.js
Centralized Error Handling
// middleware/errorHandler.js
function errorHandler(err, req, res, next) {
const status = err.statusCode || 500;
res.status(status).json({ error: err.message || 'Internal Server Error' });
}
module.exports = errorHandler;
Every route wraps async logic in a try/catch and calls next(err) instead of handling errors inline. One handler, registered last in app.js, catches everything.
Input Validation Before It Hits Business Logic
const { z } = require('zod');
const createUserSchema = z.object({
email: z.string().email(),
name: z.string().min(2),
});
function validate(schema) {
return (req, res, next) => {
const result = schema.safeParse(req.body);
if (!result.success) return res.status(400).json({ error: result.error.issues });
req.body = result.data;
next();
};
}
Non-Negotiables for Production
- Rate limiting on public endpoints (
express-rate-limit) - Helmet for sensible default security headers
- Structured logging (pino or winston) instead of
console.log - Graceful shutdown handling on
SIGTERMso in-flight requests finish before the process exits
None of this is exotic — it's the boring, repeatable checklist that turns a demo API into one you can actually put behind a load balancer.