Modular Routers at Scale

Split a large API into feature routers, each owning its own middleware, and assemble them with a single mount point.

A 2,000-line app.js is a code smell. The cure is feature-oriented routers: each feature (users, orders, billing) exports its own express.Router() that owns its routes and its route-local middleware. The app then becomes a short, readable manifest of mounts.

Routers are mini-apps

A router can carry its own parsers, validators and even its own error handling. Because app.use('/orders', ordersRouter) prefixes every path inside, the router never repeats /orders.

// routes/index.js — one place that lists the whole surface area
import { Router } from 'express';
import users from './users.js';
import orders from './orders.js';

const api = Router();
api.use('/users', users);
api.use('/orders', orders);
export default api; // app.use('/api/v1', api)

Express 5 routing changes to know

  • Path matching moved to path-to-regexp v8: the bare * wildcard now needs a name, e.g. /files/*splat.
  • Optional segments use braces: /users{/:id} instead of the old /users/:id?.
  • Regex must be explicit; unnamed groups are gone.

Example

Example · javascript
// routes/orders.js — a self-contained feature router.
import express, { Router } from 'express';
import * as orders from '../controllers/orders.js';
import { validate } from '../middleware/validate.js';
import { createOrderSchema } from '../schemas/orders.js';

const router = Router({ mergeParams: true });

// Router-local middleware: applies to every route in THIS file only.
router.use(express.json({ limit: '50kb' }));

// Express 5 path syntax: optional id via braces, named wildcard for files.
router
  .route('/{/:id}')
  .get(orders.list)          // GET /orders and GET /orders/:id
  .post(validate(createOrderSchema), orders.create);

router.get('/:id/attachments/*splat', orders.attachment);

export default router;

// app.js
// import api from './routes/index.js';
// app.use('/api/v1', api);   // version lives at the mount, not the route

When to use it

  • A large Express monolith is refactored into feature-based route modules so the authentication, products, and orders domains each live in separate files.
  • A team uses nested routers so the /api/v1 router mounts feature routers, and each feature router mounts its own sub-resource routers.
  • A developer writes integration tests for the orders router in isolation by importing the Router directly without mounting the full app.

More examples

Feature-based router modules

Each resource gets its own Router file; app.js only mounts them, keeping the entry point short.

Example · js
// routes/products.js
const router = require('express').Router();
const ctrl = require('../controllers/products');
router.get('/',    ctrl.list);
router.post('/',   ctrl.create);
router.get('/:id', ctrl.getById);
module.exports = router;

// app.js
app.use('/api/products', require('./routes/products'));
app.use('/api/orders',   require('./routes/orders'));

Nested sub-resource router

Mounts an order items router under /:id/items with mergeParams: true so it inherits the parent :id parameter.

Example · js
const orderItemsRouter = require('express').Router({ mergeParams: true });
orderItemsRouter.get('/',    ctrl.listItems);
orderItemsRouter.post('/',   ctrl.addItem);
orderItemsRouter.delete('/:itemId', ctrl.removeItem);

const ordersRouter = require('express').Router();
ordersRouter.get('/',    ctrl.listOrders);
ordersRouter.get('/:id', ctrl.getOrder);
ordersRouter.use('/:id/items', orderItemsRouter); // nested

app.use('/api/orders', ordersRouter);

API version barrel router

A v1 index router mounts all v1 feature routers; swapping to v2 only requires changing the mount point in app.js.

Example · js
// routes/v1/index.js
const router = require('express').Router();
router.use('/users',    require('./users'));
router.use('/products', require('./products'));
router.use('/orders',   require('./orders'));
module.exports = router;

// app.js
app.use('/api/v1', require('./routes/v1'));
app.use('/api/v2', require('./routes/v2'));

Discussion

  • Be the first to comment on this lesson.