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.
| Need | How |
|---|---|
| Run something before the handler | Do it, then return next(). |
| Reject the request | Throw an HttpException, or return a Response without calling next(). |
| Change the response | const res = await next(), then modify or replace it. |
| Share data with the handler | ctx.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 routeThey 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.