Express Router
Express Router is a mini-app you mount under a base path. Use it to split a monolithic app.js into per-resource files (users.js, orders.js), share middleware within a slice, and keep route definitions co-located with their controllers. The pattern scales much better than one giant file.
Modular routers with middleware and validation
EXAMPLE
// src/server.js — the app composes routers; it does not own them
const express = require('express');
const usersRouter = require('./routes/users');
const ordersRouter = require('./routes/orders');
const { errorHandler } = require('./middleware/errors');
const app = express();
app.use(express.json({ limit: '64kb' }));
app.use('/api/users', usersRouter);
app.use('/api/orders', ordersRouter);
// 404 fallthrough
app.use((req, res) => res.status(404).json({ error: 'not found' }));
app.use(errorHandler);
app.listen(3000, () => console.log('listening :3000'));
// src/routes/users.js — one router per resource
const router = require('express').Router();
const { requireAuth } = require('../middleware/auth');
const { validate } = require('../middleware/validate');
const ctrl = require('../controllers/users');
const { z } = require('zod');
// Middleware scoped to JUST this router — applies to every route below
router.use(requireAuth());
// Param middleware — runs whenever :id appears in the path
router.param('id', async (req, _res, next, id) => {
req.user = await ctrl.byId(id);
if (!req.user) return next({ status: 404, message: 'user not found' });
next();
});
router.get('/', ctrl.list);
router.get('/:id', ctrl.show);
router.post('/',
validate(z.object({ email: z.string().email(), name: z.string().min(1) })),
ctrl.create
);
router.patch('/:id',
validate(z.object({ name: z.string().min(1).optional(),
email: z.string().email().optional() })),
ctrl.update
);
router.delete('/:id', ctrl.destroy);
module.exports = router;
// src/middleware/validate.js — Zod-backed request validation
exports.validate = (schema) => (req, _res, next) => {
const parsed = schema.safeParse(req.body);
if (!parsed.success) return next({ status: 400, message: 'validation failed', issues: parsed.error.issues });
req.body = parsed.data;
next();
};
// src/middleware/errors.js — central JSON error response
exports.errorHandler = (err, _req, res, _next) => {
const status = err.status ?? 500;
if (status >= 500) console.error(err);
res.status(status).json({ error: err.message ?? 'internal', issues: err.issues });
};
// src/controllers/users.js
exports.list = async (_req, res) => res.json(await db.users.findMany());
exports.show = async (req, res) => res.json(req.user);
exports.create = async (req, res) => res.status(201).json(await db.users.create(req.body));
exports.update = async (req, res) => res.json(await db.users.update(req.user.id, req.body));
exports.destroy= async (req, res) => { await db.users.delete(req.user.id); res.status(204).end(); };
exports.byId = (id) => db.users.findById(id);
Why it matters
Co-locating the router, middleware, and controller for one resource in a folder ("feature folders") scales further than splitting by technical layer. The unit you most often need to add, refactor, or remove is a *resource*, not "all controllers" or "all routes" — make the directory structure match.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
import { Router } from 'express';
const users = Router();
users.get('/', (req, res) => res.json(allUsers));
app.use('/users', users);
Try it Yourself »
Discussion
Loading…