Skip to content
Express.js ex routing 4 min read

The Express Router

As an application grows, defining every route directly on the app object turns a single file into a sprawling mess. The Express Router solves this by letting you group related routes into a self-contained, mountable unit — a kind of mini-application that you can build in its own file and attach to the main app wherever you like. express.Router() gives you the same routing methods as app (get, post, use, and friends), so the code you already know works identically, just organized into clean, focused modules.

Creating a router

You create a router by calling express.Router(). The returned object behaves like a stripped-down app: you register routes and middleware on it exactly the way you would on the application itself.

const express = require("express");
const router = express.Router();

router.get("/", (req, res) => {
  res.json([{ id: 1, name: "Ada" }]);
});

router.post("/", (req, res) => {
  res.status(201).json({ id: 2, name: "Grace" });
});

module.exports = router;

Notice the paths are written relative to wherever the router will eventually be mounted. The "/" above is not the site root — it becomes whatever prefix you give the router at mount time.

Mounting a router

A router does nothing until it is attached to an app (or to another router) with app.use(). The first argument is the mount path, a prefix that is prepended to every route the router defines.

const express = require("express");
const usersRouter = require("./routes/users");

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

app.use("/users", usersRouter);

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

With the router mounted at /users, the router.get("/") handler now answers GET /users, and router.post("/") answers POST /users. A route declared as router.get("/:id") would respond to GET /users/:id. The mount path and the route path are simply concatenated.

Output:

$ curl http://localhost:3000/users
[{"id":1,"name":"Ada"}]

Tip: The mount path is stripped from req.url inside the router but preserved on req.baseUrl. So in the /users router, a request to /users/42 sees req.baseUrl === "/users" and req.url === "/42". This is what makes a router truly relocatable — move the mount path and every route moves with it.

Splitting routes into files

The real payoff is structure. Put each resource in its own file under a routes/ directory, export the router, and wire them together in your entry point.

// routes/products.js
const express = require("express");
const router = express.Router();

router.get("/", async (req, res, next) => {
  try {
    const products = await db.products.findAll();
    res.json(products);
  } catch (err) {
    next(err);
  }
});

router.get("/:id", async (req, res, next) => {
  try {
    const product = await db.products.findById(req.params.id);
    if (!product) return res.status(404).json({ error: "Not found" });
    res.json(product);
  } catch (err) {
    next(err);
  }
});

module.exports = router;

The entry file stays short and reads like a table of contents:

// app.js
const express = require("express");
const app = express();

app.use(express.json());

app.use("/users", require("./routes/users"));
app.use("/products", require("./routes/products"));
app.use("/orders", require("./routes/orders"));

app.use((req, res) => res.status(404).json({ error: "Route not found" }));

app.listen(3000);

Router-level middleware

Because a router is its own pipeline, you can attach middleware that runs only for routes within that router. This is perfect for concerns that apply to a whole resource — authentication, logging, or loading a shared record.

const router = express.Router();

// Runs before every route in this router only
router.use((req, res, next) => {
  console.log(`[users] ${req.method} ${req.originalUrl}`);
  next();
});

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

module.exports = router;

app vs. Router

The two APIs overlap heavily, but a router is intentionally lighter. The table below summarizes the differences.

Capabilityappexpress.Router()
Route methods (.get, .post, …)YesYes
.use() for middlewareYesYes
Can be mounted with app.use(path, …)NoYes
.listen() to bind a portYesNo
Settings via .set() / .get(name)YesNo
View engine / app.render()YesNo

Note: In Express 5, the wildcard mount syntax changed — a catch-all parameter must be named (use /:rest(*) or /*splat) rather than the bare * that Express 4 accepted. Plain string mount paths like /users are unchanged across versions.

Best Practices

  • Keep one router per resource or feature, each in its own file under a routes/ directory.
  • Write route paths relative to the mount point — never hardcode the prefix inside the router, so the router stays relocatable.
  • Mount routers in your entry file and keep that file as a thin index of app.use() calls.
  • Attach cross-cutting middleware (auth, logging) at the router level so it applies to the whole resource automatically.
  • Use async handlers with try/catch and next(err), or rely on Express 5’s automatic promise-rejection forwarding.
  • Register a final catch-all 404 handler on the app after all routers are mounted.
Last updated June 14, 2026
Was this helpful?