Notifications

No notifications

/Phase 2

Advanced Routing

Express Router — Modular Routes

The Express Router lets you split routes into separate files for cleaner code organization.

Router Basics

// routes/users.js
const router = express.Router();

router.get('/', listUsers); // GET /api/users router.get('/:id', getUser); // GET /api/users/:id router.post('/', createUser); // POST /api/users router.put('/:id', updateUser); // PUT /api/users/:id router.delete('/:id', deleteUser); // DELETE /api/users/:id

module.exports = router;

// server.js app.use('/api/users', userRoutes);

Route Parameters

TypeExample URLAccess
Params/users/42req.params.id → '42'
Query/users?page=1&limit=10req.query.page → '1'
BodyPOST datareq.body.name

Route Chaining

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

On this page

Detailed Theory

What Routing Means

A route says "when a request matches *this method* and *this URL pattern*, run *this function*." Express routing is a thin DSL for that:

app.get   ('/users',     listUsers);
app.post  ('/users',     createUser);
app.get   ('/users/:id', getUser);
app.patch ('/users/:id', updateUser);
app.delete('/users/:id', deleteUser);

That's a complete CRUD resource in 5 lines.

Path Parameters

A segment that starts with : is a placeholder. Express puts the captured value on req.params:

app.get('/users/:id', (req, res) => {
  res.json({ id: req.params.id });
});
// GET /users/42 → { id: "42" }

Multiple params? Each one becomes a key:

app.get('/users/:userId/posts/:postId', (req, res) => {
  const { userId, postId } = req.params;
});

Query Strings

Everything after ? is parsed into req.query:

app.get('/search', (req, res) => {
  const { q, page = '1', limit = '10' } = req.query;
  res.json({ q, page: Number(page), limit: Number(limit) });
});
// GET /search?q=react&page=2 → { q: "react", page: 2, limit: 10 }

Query values are always strings (or string arrays). Convert them yourself.

Routers — Splitting a Big API

A single server.js with 50 routes becomes unreadable. Use Router to group related routes:

// routes/users.routes.js
import { Router } from 'express';
const router = Router();

router.get ('/', listUsers); router.post ('/', createUser); router.get ('/:id', getUser);

export default router;

// app.js
import usersRouter from './routes/users.routes.js';
app.use('/api/users', usersRouter);   // GET /api/users, POST /api/users, …

A router is itself middleware, so you can stack auth on top:

app.use('/api/admin', requireAdmin, adminRouter);

route() Chaining

When one URL has many methods, group them:

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

Easier to read than three separate router.get / router.patch / router.delete lines.

Beginner Mistakes to Skip

1. Confusing req.params and req.query. /users/42 → params. /users?id=42 → query. 2. Forgetting numbers are strings. req.params.id === 42 is false. Convert with Number(…) or use validation (Zod). 3. Route order traps. app.get('/users/:id', …) declared before app.get('/users/me', …) swallows /users/me (id = "me"). Put specific before generic. 4. Trailing-slash inconsistency. /users and /users/ are technically different. Pick one and redirect the other (or use a normaliser). 5. Hard-coding versions in every file. Mount v1Router once at /api/v1. 6. Putting business logic in the route. Routes should hand off to a controller / service. Otherwise you can't unit-test it.

Intermediate: Nested Routers + mergeParams

When you mount one router under a parameterised path, the child needs to *see* the parent's params:

// /api/users/:userId/posts/...
import { Router } from 'express';
const posts = Router({ mergeParams: true });   // ← the magic flag

posts.get('/', (req, res) => { const { userId } = req.params; // available now res.json({ userId, posts: [] }); });

router.use('/:userId/posts', posts);

Without mergeParams: true, req.params.userId would be undefined inside the child router.

Intermediate: Controller Pattern

Keep routes one-liners, push logic into controllers:

// controllers/users.controller.js
export const listUsers = async (req, res, next) => {
  try {
    const users = await userService.list(req.query);
    res.json({ data: users });
  } catch (e) { next(e); }
};

// routes/users.routes.js
import { listUsers, createUser, getUser } from '../controllers/users.controller.js';
router.get('/',    listUsers);
router.post('/',   createUser);
router.get('/:id', getUser);

Now the controller is unit-testable without spinning up Express.

Intermediate: API Versioning

Never break existing clients. Mount versions side-by-side:

app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);

Deprecate v1 with a header (Deprecation: true, Sunset: ) and a docs link.

Intermediate: router.param — DRY Up Param Loading

router.param('id', async (req, res, next, id) => {
  const user = await User.findById(id);
  if (!user) return res.status(404).json({ error: 'not found' });
  req.user = user;
  next();
});

router.get('/:id', (req, res) => res.json(req.user)); router.patch('/:id', updateUser); router.delete('/:id', deleteUser);

One lookup, three handlers benefit. Stops every controller re-querying the user.

Intermediate: 404 & Catch-All

Always add a final catch-all so unmatched URLs get a clean JSON error:

app.use((req, res) => {
  res.status(404).json({ error: Cannot ${req.method} ${req.originalUrl} });
});

In Express 5, use app.all('/*splat', …) instead of the legacy '*' (which now throws).

Advanced: Path Patterns & Regex

Express 5 uses path-to-regexp v8. A few useful tricks:

// Optional segment
router.get('/posts/:slug?', …);              // /posts and /posts/foo

// Repeated / catch-all router.get('/files/*splat', …); // /files/a/b/c → splat = ['a','b','c']

// Custom regex on a param ("only digits") router.get('/users/:id(\\d+)', …);

Reach for these only when needed — most APIs do fine with plain :param.

Advanced: Content Negotiation

Serve HTML or JSON from the same route based on Accept:

app.get('/users/:id', async (req, res) => {
  const user = await User.findById(req.params.id);
  res.format({
    'application/json': () => res.json(user),
    'text/html':        () => res.render('user', { user }),
    default:            () => res.status(406).send('Not Acceptable'),
  });
});

Advanced: Generating URLs (req.originalUrl, base URLs)

req.originalUrl   // "/api/users/42?x=1" — full incoming URL
req.baseUrl       // "/api/users"        — prefix this router was mounted at
req.path          // "/42"               — within the router

Useful for building HATEOAS links, redirects, and pagination URLs.

Advanced: OpenAPI / Schema-Driven Routing

Libraries like express-openapi-validator, zod-to-openapi, or Fastify's built-in JSON-schema take a schema and:

  • Validate req.body / req.query automatically.
  • Generate /docs (Swagger UI) for free.
  • Produce a typed client SDK.
For any API you'll maintain longer than a weekend, this pays for itself fast.

Practice Path

1. Build a Router for /api/posts with GET /, POST /, GET /:id, DELETE /:id. 2. Add a nested router /api/posts/:postId/comments using mergeParams: true. 3. Use router.param('postId', …) to load the post once and stick it on req.post. 4. Add a catch-all 404 JSON handler at the end and a versioned mount app.use('/api/v1', …).