iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

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…