Building RESTful APIs with Node.js and Express: A Production Guide
Express is minimal by design, which means you have to make all the right decisions yourself. Here is how I structure production-grade REST APIs in Node.js.
Building RESTful APIs with Node.js and Express: A Production Guide
Express.js gives you almost nothing out of the box — which is either its greatest strength or its greatest weakness, depending on how you look at it. It forces you to make explicit decisions about every aspect of your API, which means you understand everything in your stack. It also means you can make bad decisions very easily.
Here is how I build production-grade REST APIs with Node.js and Express, based on patterns I have refined across multiple real projects.
Project Structure
Flat structure does not scale. I use a layered architecture:
src/
routes/ # Express route definitions — thin, just wire up controllers
controllers/ # Request parsing, response formatting, no business logic
services/ # Business logic — no Express dependencies
repositories/ # Database access — no business logic
middleware/ # Auth, logging, validation, error handling
models/ # Mongoose/Prisma models and types
utils/ # Pure utility functions
config/ # Environment configThe key constraint: services must not import from controllers, routes, or Express. Services contain pure business logic and are testable without spinning up an HTTP server.
Request Validation First
Validate every incoming request before it touches your business logic. I use Zod for schema validation:
import { z } from "zod";
import { Request, Response, NextFunction } from "express";const createUserSchema = z.object({
email: z.string().email(),
name: z.string().min(1).max(100),
password: z.string().min(8),
});
export function validateBody<T>(schema: z.ZodSchema<T>) {
return (req: Request, res: Response, next: NextFunction) => {
const result = schema.safeParse(req.body);
if (result.success) {
return res.status(400).json({
error: "Validation failed",
details: result.error.flatten()
});
}
req.body = result.data;
next();
};
}
// Usage
router.post("/users", validateBody(createUserSchema), userController.create);
`