Express Router and Project Structure

intermediate
14 min

Express Router and Project Structure

A single index.js is fine for five routes. By fifty routes it is a maintenance nightmare. Express solves this with express.Router, a mini application you can define in its own file and mount onto the main app. This lesson refactors a growing project into a clean, conventional structure you can reuse for every future Express app.

The Problem with One Big File

When routes, middleware, configuration, and business logic share one file, every change risks breaking something unrelated, merge conflicts multiply, and finding code takes longer than writing it. The fix is separation of concerns: each resource gets its own router file, and the main file only wires things together.

Meet express.Router

A router behaves like a small app. You attach routes and middleware to it, then mount it under a path prefix:

js
// routes/users.js const { Router } = require('express'); const router = Router(); router.get('/', (req, res) => res.json({ list: 'all users' })); router.get('/:id', (req, res) => res.json({ user: req.params.id })); router.post('/', (req, res) => res.status(201).json({ created: true })); module.exports = router;
js
// index.js const express = require('express'); const usersRouter = require('./routes/users'); const app = express(); app.use(express.json()); app.use('/api/users', usersRouter); app.listen(3000, () => console.log('Structured app on port 3000'));

Inside the router, paths are relative to the mount point. router.get('/') answers /api/users, and router.get('/:id') answers /api/users/42. Renaming the prefix later means editing exactly one line.

A Conventional Folder Layout

There is no single official structure, but this layered layout is widespread and scales well:

project/ index.js # app wiring and startup routes/ # HTTP layer: one router per resource users.js products.js controllers/ # request handlers with the actual logic users.controller.js services/ # business rules, independent of HTTP models/ # database schemas (lesson 10) middleware/ # custom middleware like auth and validation public/ # static assets

The guiding idea: routes decide where, controllers decide what, services decide how.

Extracting Controllers

Keeping handler bodies out of route files makes both easier to read and test:

js
// controllers/users.controller.js const users = [{ id: 1, name: 'Aisha' }]; exports.listUsers = (req, res) => res.json(users); exports.getUser = (req, res) => { const user = users.find(u => u.id === Number(req.params.id)); if (!user) return res.status(404).json({ error: 'Not found' }); res.json(user); };
js
// routes/users.js const { Router } = require('express'); const { listUsers, getUser } = require('../controllers/users.controller'); const router = Router(); router.get('/', listUsers); router.get('/:id', getUser); module.exports = router;

Now the route file reads like a table of contents for the resource.

Router-Level Middleware

Middleware attached to a router applies only to its routes, which is perfect for protecting a whole section:

js
const adminRouter = Router(); adminRouter.use(requireAuth); // guards every admin route adminRouter.get('/stats', (req, res) => res.json({ visits: 1024 })); app.use('/admin', adminRouter);

This keeps global middleware minimal and makes security boundaries explicit.

Key Takeaways

  • express.Router creates mountable mini-apps; mount them with app.use(prefix, router).
  • Paths inside a router are relative to the mount prefix.
  • Split code into routes, controllers, and services for readable, testable projects.
  • Router-level middleware protects an entire section in one line.
  • index.js should only wire pieces together, not contain business logic.

Next lesson: Template Engines with EJS, where the server renders dynamic HTML pages.

Express Router and Project Structure - Express.js | CodeYourCraft | CodeYourCraft