Express Routers & Route Organization

Reviewed & published by Brayan K

An Express Router is a mini-application of grouped routes that you mount under a shared URL prefix, letting you split a growing API into small, focused files.

Part of the free Node.js course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

In this lesson you'll create routers with express.Router(), mount them with app.use('/api', router), read route params, chain methods with app.route(), split routes into files, and understand why route order matters.

What You'll Learn in This Lesson

1️⃣ Creating & Mounting a Router

Putting every route directly on app gets messy fast. express.Router() creates a mini-app: a self-contained group of routes you define with relative paths. You then mount it with app.use('/api/users', router), and every route inside it gains that prefix.

Route parameters — the :id in a path — arrive on req.params. Note they're always strings, so convert with Number() when you need a number.

import express from 'express';
import request from 'supertest';   // lets us call the app in-process

// A Router is a mini-app: a self-contained group of routes you can
// mount under a shared prefix. In a real project this lives in its
// own file, e.g. routes/users.js, and is exported.
const userRouter = express.Router();
userRouter.get('/', (req, res) => res.json({ users: ['ada', 'linus'] }));
userRouter.get('/:id', (req, res) => res.json({ id: req.params.id }));

const app = express();
// Mount it: every route inside now lives under /api/users
app.use('/api/users', userRouter);

const r1 = await request(app).get('/api/users');
console.log('GET /api/users ->', r1.status, JSON.stringify(r1.body));
const r2 = await request(app).get('/api/users/42');
console.log('GET /api/users/42 ->', r2.status, JSON.stringify(r2.body));

2️⃣ Chaining Methods with app.route()

When several HTTP methods share the same path — say GET, PUT, and DELETE on /books/:id — repeating that string on every line is noisy. app.route(path) lets you declare the path once and chain the method handlers off it.

import express from 'express';
import request from 'supertest';

const app = express();
app.use(express.json());

const books = [{ id: 1, title: 'Node Basics' }];

// app.route() chains several HTTP methods onto ONE path so you don't
// repeat the string '/books/:id' on every line.
app.route('/books/:id')
  .get((req, res) => {
    const book = books.find(b => b.id === Number(req.params.id));
    res.json(book || { error: 'not found' });
  })
  .delete((req, res) => res.json({ deleted: Number(req.params.id) }));

const g = await request(app).get('/books/1');
console.log('GET /books/1 ->', g.status, JSON.stringify(g.body));
const d = await request(app).delete('/books/1');
console.log('DELETE /books/1 ->', d.status, JSON.stringify(d.body));

3️⃣ Route Order & Splitting Into Files

Express checks routes in the order you declare them and stops at the first match. That means a literal path like /users/me must come before a parameter path like /users/:id — otherwise :id matches first with id = "me" and your specific handler never runs.

import express from 'express';
import request from 'supertest';

const app = express();

// Express matches routes TOP TO BOTTOM and stops at the first hit.
// So a specific path ('/users/me') must be declared BEFORE a
// parameter path ('/users/:id'), or ':id' would swallow 'me'.
app.get('/users/me', (req, res) => res.json({ who: 'current user' }));
app.get('/users/:id', (req, res) => res.json({ id: req.params.id }));

const me = await request(app).get('/users/me');
console.log('GET /users/me ->', JSON.stringify(me.body));
const id = await request(app).get('/users/7');
console.log('GET /users/7  ->', JSON.stringify(id.body));

In real projects you put each feature's router in its own file and mount them all in app.js. Here's the standard structure (shown as code — it spans multiple files, so it isn't run on one page):

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

router.get('/', (req, res) => res.json(getAllProducts()));
router.get('/:id', (req, res) => res.json(getProduct(req.params.id)));
router.post('/', (req, res) => res.status(201).json(createProduct(req.body)));

export default router;

// app.js  -----------------------------------------------------
import express from 'express';
import productRouter from './routes/products.js';
import userRouter from './routes/users.js';

const app = express();
app.use(express.json());

// Each feature gets its own prefix and its own file.
app.use('/api/products', productRouter);
app.use('/api/users', userRouter);

app.listen(3000, () => console.log('API on http://localhost:3000'));

Your turn. Fill in the two blanks marked ___ to create and mount a router, then run it and compare with the expected output.

import express from 'express';
import request from 'supertest';

// 🎯 YOUR TURN — fill in the two ___ blanks, then run it.

// 1) Create a router.
const router = express.___();            // 👉 Router

router.get('/ping', (req, res) => res.json({ ok: true }));

const app = express();

// 2) Mount the router under the /api prefix.
app.use('___', router);                  // 👉 '/api'

const r = await request(app).get('/api/ping');
console.log(r.status, JSON.stringify(r.body));

// ✅ Expected output:
//    200 {"ok":true}

Mini-Challenge: A Products Router

No blanks this time — build the router yourself from the brief. Add a list route, a by-id route with a 404 fallback, and mount it at /api/products. Run it and check your output against the example in the comments.

import express from 'express';
import request from 'supertest';

// 🎯 MINI-CHALLENGE: a products router
// 1. Build a Router with:
//      GET '/'      -> respond with the whole products array
//      GET '/:id'   -> find by Number(id); if missing, respond 404
//                      with { error: 'not found' }, else the product
// 2. Mount it at '/api/products'.
// 3. The three requests below should print 200, 200, and 404.
//
// ✅ Example output:
//    list -> 200 [{"id":1,"name":"Pen"},{"id":2,"name":"Notebook"}]
//    one  -> 200 {"id":2,"name":"Notebook"}
//    miss -> 404 {"error":"not found"}

const products = [{ id: 1, name: 'Pen' }, { id: 2, name: 'Notebook' }];

const productRouter = express.Router();
// your routes here...

const app = express();
// mount it here...

const all = await request(app).get('/api/products');
console.log('list ->', all.status, JSON.stringify(all.body));
const one = await request(app).get('/api/products/2');
console.log('one  ->', one.status, JSON.stringify(one.body));
const miss = await request(app).get('/api/products/99');
console.log('miss ->', miss.status, JSON.stringify(miss.body));

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — Express Routing

TaskSyntax
Create a routerconst r = express.Router()
Mount under a prefixapp.use('/api', r)
Read a route paramreq.params.id
Read a query valuereq.query.page
Chain methods on a pathapp.route('/x').get(...).put(...)
Parse JSON bodiesapp.use(express.json())

🎉 Lesson Complete!

Practice quiz

What is an express.Router()?

  • A database table
  • A self-contained mini-app of routes you can mount under a prefix
  • A middleware that parses JSON
  • A logging tool

Answer: A self-contained mini-app of routes you can mount under a prefix. A Router groups related routes into a mountable mini-app with relative paths.

How do you mount a router so its routes live under /api/users?

  • app.get('/api/users', router)
  • app.mount('/api/users', router)
  • router.use('/api/users', app)
  • app.use('/api/users', router)

Answer: app.use('/api/users', router). app.use('/api/users', router) mounts the router; each inner route gains that prefix.

Inside a router mounted at /api/users, what path do you write for the by-id route?

  • '/:id' (relative to the mount point)
  • '/api/users/:id'
  • '/users/:id'
  • 'api/users/:id'

Answer: '/:id' (relative to the mount point). Router paths are relative to the mount point, so you write '/:id', not the full path.

Why does /users/:id declared before /users/me break the /users/me route?

  • Parameter routes are disabled
  • Express sorts routes alphabetically
  • The :id route matches first with id = 'me', so /users/me never runs
  • Routers cannot have literal paths

Answer: The :id route matches first with id = 'me', so /users/me never runs. Express stops at the first match, so the earlier :id route swallows 'me' as the id.

What does app.route('/books/:id') let you do?

  • Mount another app
  • Parse the request body
  • Declare the path once and chain .get(), .delete(), etc.
  • Redirect to a new URL

Answer: Declare the path once and chain .get(), .delete(), etc.. app.route(path) declares the path once and chains multiple method handlers off it.

Values in req.params arrive as what type?

  • Numbers
  • Strings
  • Booleans
  • Objects

Answer: Strings. Route params are always strings; convert with Number() when you need a number.

Which middleware must you add so req.body is populated on a JSON POST?

  • express.static()
  • express.urlencoded() only
  • app.use(express.json())
  • express.router()

Answer: app.use(express.json()). express.json() parses a JSON request body into req.body; without it req.body is undefined.

Your router's routes all return 404. What is the most likely cause?

  • You forgot to mount it with app.use(prefix, router)
  • You used app.route()
  • The port is wrong
  • You forgot express.json()

Answer: You forgot to mount it with app.use(prefix, router). Defining a router doesn't connect it; you must mount it with app.use before its routes respond.

What does a double prefix like /api/users/api/users usually mean?

  • The port is duplicated
  • express.json() is missing
  • Two routers share a prefix
  • You wrote the full path inside the mounted router instead of a relative one

Answer: You wrote the full path inside the mounted router instead of a relative one. Inside a mounted router use relative paths like '/' and '/:id', not the full prefixed path.

What is the recommended structure for a non-trivial Express API?

  • One Router per feature, each in its own file, mounted in app.js
  • All routes in a single giant function
  • One file per route handler with no routers
  • Define routes inside app.listen()

Answer: One Router per feature, each in its own file, mounted in app.js. Create one Router per feature in a routes/ folder and mount them all in app.js.

Continue this course

Frequently asked questions

What is the difference between app.use and app.get with a Router?

app.get('/path', handler) registers a single GET route directly on the app. app.use('/prefix', router) MOUNTS a whole Router under a prefix — every route defined inside that router becomes prefix + its own path. So a router with router.get('/:id') mounted at app.use('/api/users', router) responds to GET /api/users/:id. Use app.use with a Router to group and prefix many related routes at once; use app.get for one-off endpoints.

Why does my /users/:id route match before /users/me?

Express evaluates routes in the order you declare them and stops at the first match. If app.get('/users/:id') is listed before app.get('/users/me'), then a request to /users/me matches the :id route first (with id = 'me') and your /users/me handler never runs. The fix is ordering: declare specific, literal routes BEFORE parameterized ones. As a rule, put the most specific paths at the top of the file.

How do I read route parameters and query strings?

Route parameters — the :id parts of a path — come from req.params (for example req.params.id). Query string values after the ? come from req.query (for example req.query.page for /search?page=2). The JSON body of a POST/PUT comes from req.body, but only after you add the express.json() middleware. All three are plain objects, and their values arrive as strings, so convert with Number() when you need a number.

Should I split my routes into separate files?

Yes, once you have more than a handful of endpoints. Create one Router per feature (users, products, orders), keep each in its own file under a routes/ folder, export the router, and mount them in app.js with app.use('/api/users', userRouter) and so on. This keeps each file small and focused, makes routes easy to find, and lets you change one feature's URLs in a single place. It's the standard structure for any non-trivial Express API.