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.
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.
A router behaves like a small app. You attach routes and middleware to it, then mount it under a path prefix:
// 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;// 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.
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.
Keeping handler bodies out of route files makes both easier to read and test:
// 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);
};// 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.
Middleware attached to a router applies only to its routes, which is perfect for protecting a whole section:
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.
Next lesson: Template Engines with EJS, where the server renders dynamic HTML pages.