Conditional Middleware

Run Middleware Only When the Path or Method Matches

Conditional Middleware

Not every middleware should run on every request. Learn to gate middleware execution by inspecting ctx.path or ctx.method, and how the koa-unless pattern inverts the condition cleanly.

3 min read Level 2/5 #koa#middleware#conditional
What you'll learn
  • Conditionally invoke next() based on ctx.path and ctx.method
  • Implement an unless wrapper to skip middleware for specified routes
  • Apply conditional middleware for auth, logging, and body parsing

app.use() registers middleware for every request. To restrict execution to certain paths or methods, inspect ctx.path and ctx.method before deciding whether to invoke next() or do real work.

Branching on ctx.path

The simplest approach: check the path at the top of the middleware.

app.use(async (ctx, next) => {
  if (ctx.path.startsWith('/api')) {
    // only run for /api/* routes
    ctx.state.apiRequest = true;
  }
  await next(); // always call next so other routes still run
});

Skipping Middleware for Specific Routes

Sometimes you want a middleware to run everywhere except a few paths — e.g., skip auth for /health or /login.

function unless(middleware, ...excludedPaths) {
  return async (ctx, next) => {
    if (excludedPaths.includes(ctx.path)) {
      return next(); // skip the middleware, pass through
    }
    return middleware(ctx, next);
  };
}

import auth from './auth-middleware.js';

app.use(unless(auth, '/health', '/login', '/register'));

Gating on ctx.method

import bodyParser from 'koa-bodyparser';

// Only parse body for mutating requests
app.use(async (ctx, next) => {
  if (['POST', 'PUT', 'PATCH'].includes(ctx.method)) {
    return bodyParser()(ctx, next);
  }
  await next();
});

Combining Path and Method

app.use(async (ctx, next) => {
  const isApiWrite =
    ctx.path.startsWith('/api') && ctx.method !== 'GET';

  if (isApiWrite) {
    // enforce JSON content-type for API mutations
    if (!ctx.is('application/json')) {
      ctx.throw(415, 'Content-Type must be application/json');
    }
  }
  await next();
});

Using @koa/router for Path-Scoped Middleware

For per-route middleware, router-level attachment is cleaner than global conditionals:

import Router from '@koa/router';
const router = new Router();

async function requireAdmin(ctx, next) {
  if (!ctx.state.user?.isAdmin) ctx.throw(403);
  await next();
}

// Only /admin routes get the requireAdmin check
router.use('/admin', requireAdmin);
router.get('/admin/users', listUsers);

When to Use Each Approach

ApproachBest for
ctx.path branch in app.useBroad path-prefix rules
unless() wrapper”Run everywhere except…” logic
Router .use() per prefixRoute-specific middleware
Method checkBody parsing, CSRF for mutations only

Up Next

koa-compose lets you bundle several middleware functions into a single reusable unit — ideal for shared stacks across apps.

koa-compose →