Express Router & Project Organization

📖 Concept

Express Router is a mini-application that handles routing and middleware for a specific path prefix. It enables modular, scalable project organization by splitting routes into separate files.

Why use Routers?

  • Separation of concerns — each resource gets its own route file
  • Middleware scoping — apply middleware only to specific route groups
  • Team collaboration — different developers work on different route modules
  • Testing — test route modules independently

Router pattern:

// routes/users.js
const router = express.Router();
router.get('/', listUsers);
router.post('/', createUser);
router.get('/:id', getUser);
export default router;

// app.js
app.use('/api/users', userRouter);  // Prefix all routes with /api/users

Router-level middleware:

// Apply auth to all routes in this router
router.use(authenticate);
// Or to specific routes
router.get('/', publicHandler);
router.post('/', authenticate, protectedHandler);

route() chaining:

router.route('/users/:id')
  .get(getUser)
  .put(updateUser)
  .delete(deleteUser);

🏠 Real-world analogy: If Express is a shopping mall, each Router is a store. The mall routes customers (requests) to the right store (router) based on the entrance they use (path prefix). Each store manages its own layout (routes) and security (middleware) independently.

💻 Code Example

codeTap to expand ⛶
1// Express Router — Modular Project Organization
2
3const express = require("express");
4
5// === routes/userRoutes.js ===
6function createUserRouter(userService) {
7 const router = express.Router();
8
9 // GET /api/users
10 router.get("/", async (req, res, next) => {
11 try {
12 const { page = 1, limit = 10, search } = req.query;
13 const users = await userService.findAll({ page, limit, search });
14 res.json(users);
15 } catch (err) {
16 next(err);
17 }
18 });
19
20 // GET /api/users/:id
21 router.get("/:id", async (req, res, next) => {
22 try {
23 const user = await userService.findById(req.params.id);
24 if (!user) {
25 return res.status(404).json({ error: "User not found" });
26 }
27 res.json({ data: user });
28 } catch (err) {
29 next(err);
30 }
31 });
32
33 // POST /api/users
34 router.post("/", async (req, res, next) => {
35 try {
36 const user = await userService.create(req.body);
37 res.status(201).json({ data: user });
38 } catch (err) {
39 next(err);
40 }
41 });
42
43 // PUT /api/users/:id
44 router.put("/:id", async (req, res, next) => {
45 try {
46 const user = await userService.update(req.params.id, req.body);
47 if (!user) {
48 return res.status(404).json({ error: "User not found" });
49 }
50 res.json({ data: user });
51 } catch (err) {
52 next(err);
53 }
54 });
55
56 // DELETE /api/users/:id
57 router.delete("/:id", async (req, res, next) => {
58 try {
59 await userService.delete(req.params.id);
60 res.status(204).end();
61 } catch (err) {
62 next(err);
63 }
64 });
65
66 return router;
67}
68
69// === routes/authRoutes.js ===
70function createAuthRouter(authService) {
71 const router = express.Router();
72
73 router.post("/register", async (req, res, next) => {
74 try {
75 const { name, email, password } = req.body;
76 const user = await authService.register({ name, email, password });
77 res.status(201).json({ data: user });
78 } catch (err) {
79 next(err);
80 }
81 });
82
83 router.post("/login", async (req, res, next) => {
84 try {
85 const { email, password } = req.body;
86 const token = await authService.login(email, password);
87 res.json({ token });
88 } catch (err) {
89 next(err);
90 }
91 });
92
93 return router;
94}
95
96// === routes/index.js — Route aggregator ===
97function createRoutes(services) {
98 const router = express.Router();
99
100 router.use("/auth", createAuthRouter(services.auth));
101 router.use("/users", createUserRouter(services.user));
102
103 return router;
104}
105
106// === app.js — Application setup ===
107function createApp(services) {
108 const app = express();
109
110 // Global middleware
111 app.use(express.json());
112
113 // Health check
114 app.get("/health", (req, res) => {
115 res.json({ status: "ok", uptime: process.uptime() });
116 });
117
118 // API routes
119 app.use("/api/v1", createRoutes(services));
120
121 // 404 handler
122 app.use((req, res) => {
123 res.status(404).json({ error: `${req.method} ${req.path} not found` });
124 });
125
126 // Error handler
127 app.use((err, req, res, next) => {
128 console.error(err.stack);
129 res.status(err.statusCode || 500).json({
130 error: err.message || "Internal server error",
131 });
132 });
133
134 return app;
135}
136
137// === server.js — Entry point ===
138// Mock services
139const services = {
140 user: {
141 findAll: async () => [{ id: 1, name: "Alice" }],
142 findById: async (id) => ({ id, name: "Alice" }),
143 create: async (data) => ({ id: Date.now(), ...data }),
144 update: async (id, data) => ({ id, ...data }),
145 delete: async (id) => true,
146 },
147 auth: {
148 register: async (data) => ({ id: 1, ...data }),
149 login: async () => "mock-jwt-token",
150 },
151};
152
153const app = createApp(services);
154app.listen(3000, () => console.log("Server on http://localhost:3000"));

🏋️ Practice Exercise

Exercises:

  1. Split an Express app into separate router modules: authRoutes.js, userRoutes.js, postRoutes.js
  2. Create a route aggregator (routes/index.js) that mounts all routers with proper prefixes
  3. Implement API versioning using routers: /api/v1/ and /api/v2/ with different handlers
  4. Use router.route() chaining for a resource with GET, PUT, DELETE on the same path
  5. Add router-level middleware that only applies to admin routes (/api/admin/*)
  6. Build a factory function pattern where routers receive dependencies (services, config) as parameters

⚠️ Common Mistakes

  • Defining overlapping paths between parent and child routers — this causes confusing double-matching of routes

  • Not passing errors to next(err) in async route handlers — use try/catch in every async handler and call next(err) in the catch block

  • Creating circular dependencies between route files — use dependency injection (factory functions) instead of requiring between route files

  • Hardcoding API prefixes in route files — let the parent app.use('/api/v1', router) handle the prefix; routes should use relative paths

  • Not testing routers in isolation — factory functions that accept services enable unit testing without a running database

💼 Interview Questions

🎤 Mock Interview

Mock interview is powered by AI for Express Router & Project Organization. Login to unlock this feature.