Skip to content

Middlewares

Clear Router Middlewares lets you run framework-level work before a route handler is called. Use it for authentication, request logging, rate limits, tenant lookup, body checks, or any other cross-cutting request step.

Clear Router accepts both function middleware and class-based middleware with a handle method.

ts
Router.get('/account', [AccountController, 'show'], [AuthMiddleware]);

Function Middlewares

Function middlewares uses the native signature of the adapter you imported.

Express

ts
import type { NextFunction, Request, Response } from 'express';

const authMiddleware = (req: Request, _res: Response, next: NextFunction) => {
  req.headers['x-auth-checked'] = 'true';
  next();
};

Router.get('/secure', ({ res }) => res.send('OK'), [authMiddleware]);

Fastify

ts
import type { FastifyReply, FastifyRequest } from 'fastify';
import type { NextFunction } from 'clear-router/types/fastify';

const authMiddleware = (
  req: FastifyRequest,
  _reply: FastifyReply,
  next: NextFunction,
) => {
  req.headers['x-auth-checked'] = 'true';
  next();
};

Router.get('/secure', () => ({ ok: true }), [authMiddleware]);

H3

ts
import type { H3Event } from 'h3';
import type { NextFunction } from 'clear-router/types/h3';

const authMiddleware = async (event: H3Event, next: NextFunction) => {
  event.context.authChecked = true;
  return next();
};

Router.get('/secure', () => ({ ok: true }), [authMiddleware]);

Hono

ts
import type { Context } from 'hono';

const authMiddleware = async (ctx: Context, next: () => Promise<void>) => {
  ctx.set('authChecked', true);
  await next();
};

Router.get('/secure', (ctx) => ctx.json({ ok: true }), [authMiddleware]);

Koa

ts
import type Koa from 'koa';

const authMiddleware: Koa.Middleware = async (ctx, next) => {
  ctx.state.authChecked = true;
  await next();
};

Router.get(
  '/secure',
  (ctx) => {
    ctx.body = { ok: true };
  },
  [authMiddleware],
);

Class-Based Middlewares

Classs middlewares are useful when the middleware has dependencies, configuration, or enough behaviour that a named class is easier to test.

The class must expose a handle method. The handle method receives the same framework-aware arguments as a function middleware for the adapter you are using.

ts
import type { NextFunction, Request, Response } from 'express';

class AuthMiddleware {
  handle(req: Request, _res: Response, next: NextFunction): void {
    req.headers['x-auth-checked'] = 'true';
    next();
  }
}

Router.get('/secure', ({ res }) => res.send('OK'), [AuthMiddleware]);

Clear Router instantiates middleware classes when routes are registered and binds handle to the instance before passing it to the underlying framework.

Middleware instances are supported too:

ts
class AuditMiddleware {
  constructor(private readonly source: string) {}

  handle(req: Request, _res: Response, next: NextFunction): void {
    req.headers['x-audit-source'] = this.source;
    next();
  }
}

Router.post(
  '/payments',
  [PaymentController, 'create'],
  [new AuditMiddleware('billing')],
);

Decorator Middlewares

When you register routes with controllers, middleware can be declared directly on the controller class or on individual controller methods with the @middleware decorator. This keeps a controller's middleware next to the code it protects instead of repeating it at every route registration.

ts
import { middleware } from 'clear-router/decorators';

@middleware([AuthMiddleware])
class AccountController {
  show({ res }) {
    return res.json({ ok: true });
  }
}

Router.get('/account', [AccountController, 'show']);

Class-level decorators apply to every action of the controller, including routes generated by Router.apiResource(). Method-level decorators apply only to that action:

ts
class LoginController {
  @middleware(GuestMiddleware)
  create({ res }) {
    return res.json({ ok: true });
  }

  destroy({ res }) {
    return res.json({ ok: true });
  }
}

Router.post('/login', [LoginController, 'create']); // runs GuestMiddleware
Router.delete('/login', [LoginController, 'destroy']); // no middleware

The decorator accepts every shape a route middleware does — callbacks, middleware classes, and instances exposing a handle method — either variadically or as an array:

ts
@middleware(AuthMiddleware, new AuditMiddleware('billing'))
class PaymentController {
  @middleware([RateLimitMiddleware])
  create({ res }) {
    return res.json({ ok: true });
  }
}

Decorator middleware run after global and group middleware but before any middleware passed at the route registration site. For a controller route the full order is:

txt
Global -> Group -> @middleware (class) -> @middleware (method) -> Route middleware -> Handler

TIP

@middleware uses the same metadata mechanism as @Bind, so it works with both TypeScript's experimentalDecorators and the TC39 standard decorators.

Framework-Aware Types

Each adapter exports a Middleware type that accepts either a function middleware or a class/instance with a compatible handle method.

ts
import type { Middleware } from 'clear-router/types/express';
import type { NextFunction, Request, Response } from 'express';

class AuthMiddleware {
  handle(req: Request, _res: Response, next: NextFunction): void {
    next();
  }
}

const middleware: Middleware = AuthMiddleware;

The same pattern works for every adapter:

ts
import type { Middleware as ExpressMiddleware } from 'clear-router/types/express';
import type { Middleware as FastifyMiddleware } from 'clear-router/types/fastify';
import type { Middleware as H3Middleware } from 'clear-router/types/h3';
import type { Middleware as HonoMiddleware } from 'clear-router/types/hono';
import type { Middleware as KoaMiddleware } from 'clear-router/types/koa';

Use the type from the adapter you are registering routes with. That keeps handle aligned with the framework's real middleware signature.

Route Middlewares

Pass middlewares as the third argument to any route helper.

ts
Router.get('/profile', [ProfileController, 'show'], [AuthMiddleware]);
Router.post(
  '/profile',
  [ProfileController, 'update'],
  [AuthMiddleware, AuditMiddleware],
);

You can pass a single middleware or an array:

ts
Router.get('/me', [UserController, 'show'], AuthMiddleware);
Router.get('/settings', [SettingsController, 'index'], [AuthMiddleware]);

Route helpers also return the registered route, so middlewares can be chained after the route is defined:

ts
Router.get('/profile', [ProfileController, 'show']).middleware(AuthMiddleware);

Router.post('/profile', [ProfileController, 'update']).middleware([
  AuthMiddleware,
  AuditMiddleware,
]);

The chained form is useful when route metadata is easier to read after the handler:

ts
Router.get('/reports/{report}', [ReportController, 'show'])
  .middleware(AuthMiddleware)
  .name('reports.show');

Group Middlewares

Router.group() applies middleware to every route registered inside the group.

ts
await Router.group(
  '/admin',
  () => {
    Router.get('/users', [AdminUserController, 'index']);
    Router.delete('/users/:id', [AdminUserController, 'destroy']);
  },
  [AuthMiddleware, AdminMiddleware],
);

Middleware can also be chained onto the returned RouteGroup, including groups loaded from files or directories:

ts
await Router.group('/admin', 'routes/admin').middleware([
  AuthMiddleware,
  AdminMiddleware,
]);

The group prefix and middleware are inherited by nested routes. Chained middleware is attached after every route source has finished registering.

Global Middleware Scope

Router.middleware() applies middleware to routes defined inside the callback without adding a path prefix.

ts
Router.middleware([AuthMiddleware], () => {
  Router.get('/account', [AccountController, 'show']);
  Router.get('/sessions', [SessionController, 'index']);
});

This is useful when several routes need the same middleware but do not share a URL prefix.

Resource Middlewares

Router.apiResource() accepts middlewares for all actions or per action.

ts
Router.apiResource('/posts', PostController, {
  middlewares: [AuthMiddleware],
});

Per-action middlewares use the standard resource action names:

ts
Router.apiResource('/posts', PostController, {
  middlewares: {
    index: [AuthMiddleware],
    create: [AuthMiddleware],
    destroy: [AuthMiddleware, AdminMiddleware],
  },
});

Router.apiResource() also returns the generated resource routes, so middlewares can be chained after registration:

ts
Router.apiResource('/posts', PostController).middleware(AuthMiddleware);

Resource filters can be chained from the same collection:

ts
Router.apiResource('/posts', PostController)
  .only('index', 'show')
  .middleware(AuthMiddleware);

Router.apiResource('/posts', PostController)
  .except('destroy')
  .middleware([AuthMiddleware, AuditMiddleware]);

You can target a single resource action:

ts
const posts = Router.apiResource('/posts', PostController);

posts.index()?.middleware(AuthMiddleware);
posts.destroy()?.middleware([AuthMiddleware, AdminMiddleware]);

Or target generated routes by HTTP method:

ts
const posts = Router.apiResource('/posts', PostController);

posts.get().middleware(AuthMiddleware);
posts.post().middleware([AuthMiddleware, AuditMiddleware]);

Method selections can be inspected when needed:

ts
const getRoutes = posts.get().all();
const firstGetRoute = posts.get().first();

Execution Order

Middlewares runs in the order they were registered:

txt
Global middleware -> Group middleware -> @middleware (class) -> @middleware (method) -> Route middleware -> Route handler

For example:

ts
Router.middleware([GlobalMiddleware], () => {
  Router.group(
    '/admin',
    () => {
      Router.get('/reports', [ReportController, 'index'], [RouteMiddleware]);
    },
    [GroupMiddleware],
  );
});

The /admin/reports route runs:

txt
GlobalMiddleware -> GroupMiddleware -> RouteMiddleware -> ReportController.index

Returning or Stopping Early

Middlewares follow the rules of the adapter underneath. If the framework lets middleware end the response early, Clear Router does not add a second response.

Express example:

ts
class AuthMiddleware {
  handle(req: Request, res: Response, next: NextFunction): void {
    if (!req.headers.authorization) {
      res.status(401).json({ message: 'Unauthenticated' });
      return;
    }

    next();
  }
}

Hono example:

ts
class AuthMiddleware {
  async handle(ctx: Context, next: () => Promise<void>) {
    if (!ctx.req.header('authorization')) {
      return ctx.json({ message: 'Unauthenticated' }, 401);
    }

    await next();
  }
}

Notes

  • Middlewares are normalized when the route is registered.
  • Class constructors are created with no arguments. Use an instance when you need constructor arguments.
  • handle is framework-aware, not route-context-aware. Express middleware receives (req, res, next), Hono middleware receives (ctx, next), and so on.
  • Route handlers still receive Clear Router's HttpContext after middleware completes.