Route Middleware

Run Middleware Before Specific Routes or Route Groups

Route Middleware

Attach middleware to individual routes, to a path prefix with `router.use()`, or to a named parameter with `router.param()` to share logic without repeating yourself.

4 min read Level 2/5 #koa#routing#middleware
What you'll learn
  • Attach inline middleware to a single route
  • Apply scoped middleware to a path group with `router.use()`
  • Intercept parameter extraction with `router.param()`

Middleware is not just an app-level concern. @koa/router lets you attach middleware at three levels: a single route, a path prefix, or a named parameter — so you only pay for the logic where you need it.

Per-Route Middleware

Pass one or more middleware functions before the final handler:

async function auth(ctx, next) {
  if (!ctx.headers.authorization) {
    ctx.status = 401;
    ctx.body = { error: "unauthorized" };
    return; // don't call next — stop here
  }
  await next();
}

router.get("/admin/dashboard", auth, async (ctx) => {
  ctx.body = { dashboard: true };
});

router.get("/public/info", async (ctx) => {
  ctx.body = { info: "open" }; // no auth middleware here
});

The auth function only runs for the /admin/dashboard route.

Multiple Middleware in Order

Stack as many as you need:

async function log(ctx, next) {
  console.log(`${ctx.method} ${ctx.path}`);
  await next();
}

async function validate(ctx, next) {
  if (!ctx.request.body?.name) {
    ctx.status = 400;
    ctx.body = { error: "name required" };
    return;
  }
  await next();
}

router.post("/users", log, validate, async (ctx) => {
  ctx.status = 201;
  ctx.body = { name: ctx.request.body.name };
});

router.use() — Scoped Middleware

Apply middleware to every route that starts with a path:

// Runs before all /admin/* routes
router.use("/admin", auth);

router.get("/admin/dashboard", async (ctx) => {
  ctx.body = "dashboard";
});

router.get("/admin/settings", async (ctx) => {
  ctx.body = "settings";
});

// Public route — auth does NOT run
router.get("/status", async (ctx) => {
  ctx.body = "ok";
});

Pass no path to router.use() to run middleware before every route on that router (similar to app.use()).

router.param() — Parameter Middleware

Run a function whenever a specific named parameter appears in the matched route. Great for loading the entity once and attaching it to ctx.state:

const db = new Map([["1", { id: "1", name: "Alice" }]]);

router.param("userId", async (id, ctx, next) => {
  const user = db.get(id);
  if (!user) {
    ctx.status = 404;
    ctx.body = { error: "user not found" };
    return;
  }
  ctx.state.user = user; // attach for downstream handlers
  await next();
});

router.get("/users/:userId", async (ctx) => {
  ctx.body = ctx.state.user;
});

router.patch("/users/:userId", async (ctx) => {
  ctx.state.user.name = "Updated";
  ctx.body = ctx.state.user;
});

The lookup runs once per request and both routes share the result.

Summary

TechniqueScope
Inline middlewareSingle route
router.use(path)All routes under that path
router.use()All routes on the router
router.param(key)Any route containing :key

Up Next

Sometimes a URL moves. The next lesson covers redirects and how to generate URLs from named routes.

Redirects →