Skip to Content

Middlewares

One concept for everything that wraps a request.

A middleware receives the request context and a next function. Code before next() runs on the way in, code after it on the way out.

import { middleware } from '@sukujs/core';

export const timing = middleware(async (ctx, next) => {
  const started = performance.now();
  const res = await next();
  res.headers.set('server-timing', `app;dur=${(performance.now() - started).toFixed(1)}`);
  return res;
});

That one shape covers what other frameworks split into middleware, guards and interceptors.

NeedHow
Run something before the handlerDo it, then return next().
Reject the requestThrow an HttpException, or return a Response without calling next().
Change the responseconst res = await next(), then modify or replace it.
Share data with the handlerctx.set('user', user), read with ctx.get('user').

Where they attach

Suku.create({ middlewares: [timing] });                       // every request
feature('admin', { middlewares: [authenticate] });            // a feature's routes
@Routes('/users', { middlewares: [authenticate] })            // a class's routes
route.group('/admin', { middlewares: [requireAdmin] }, {})    // a group's routes
route.post('/users', { middlewares: [rateLimit], handle })    // one route

They run outermost first: app, feature, class or group, route. app.use(...) adds app-level middlewares after creation.

App-level middlewares run for every request, including ones that match no route. next() always resolves to a Response, because errors and 404s are rendered before control returns, so a CORS or request-id middleware can decorate every response.

Ready-made middlewares for CORS, security headers, CSRF and rate limiting are in @sukujs/security.

Authentication

export const authenticate = middleware(async (ctx, next) => {
  const header = ctx.headers.get('authorization');
  if (!header) throw new UnauthorizedException();
  ctx.set('user', await verifyToken(header));
  return next();
});

Allowing or refusing

allow builds a middleware from a yes-or-no check. false becomes a 403.

import { allow } from '@sukujs/core';

const adminOnly = allow((ctx) => ctx.get<User>('user')?.role === 'admin');

route.delete('/users/:id', { middlewares: [authenticate, adminOnly], handle });

To drive a check from the route, use meta.

const requireRoles = allow((ctx) => {
  const roles = (ctx.route?.meta.roles as string[] | undefined) ?? [];
  return roles.length === 0 || roles.includes(ctx.get<User>('user')?.role ?? '');
});

route.delete('/reports/:id', { meta: { roles: ['admin'] }, handle });

Middlewares with services

When a middleware needs injected services, write it as a class. It is constructed through the container and does not need to be registered.

@Service()
export class RequestLogger implements Middleware {
  constructor(private readonly log: Logger) {}

  async use(ctx: RequestContext, next: Next) {
    const res = await next();
    this.log.info(`${ctx.method} ${ctx.url.pathname} -> ${res.status}`);
    return res;
  }
}

Suku.create({ middlewares: [RequestLogger] });

A middleware class is a singleton unless declared @Service({ lifetime: 'scoped' }), in which case it is created per request and may inject the RequestContext.

Rules

  • Return without calling next() to short-circuit.
  • Calling next() twice is an error.
  • An error thrown by a middleware goes to the error handlers.

On this page