Middleware
Middleware classes execute before the route handler and can modify requests, responses, or context.
Class Middleware
The simplest form of middleware is a class with a use() method. Use request() and response() to access HTTP primitives:
import { Middleware, request, type Next } from '@zeltjs/core';
@Middleware
export class LoggingMiddleware {
async use(next: Next, req = request()): Promise<Response | undefined> {
const start = Date.now();
await next();
const duration = Date.now() - start;
console.log(`[${req.method()}] ${req.path()} ${duration}ms`);
return undefined;
}
}
Middleware Levels
Zelt supports middleware at three levels, executed in order: global → controller → method.
Global Middleware
Apply to all routes via createApp():
export const app = createApp([http({
controllers: [UserController],
middlewares: [LoggingMiddleware],
})]);
Controller Middleware
Apply to all methods in a controller with @UseMiddleware:
@UseMiddleware(AuthMiddleware)
@Controller('/admin')
export class AdminController {
@Get('/dashboard')
dashboard() {
return { stats: [] };
}
}
Method Middleware
Apply to specific methods:
@Controller('/posts')
export class PostController {
@Get('/')
findAll() {
return { posts: [] };
}
@UseMiddleware(AdminOnlyMiddleware)
@Delete('/:id')
remove(req = request()) {
const id = req.pathParam('id');
return { deleted: id };
}
}
Skipping Middleware
Use @SkipMiddleware to exclude specific middleware from a method:
@Controller('/api')
export class ApiController {
@Get('/protected')
protected() {
return { secret: 'data' };
}
@SkipMiddleware(AuthMiddleware)
@Get('/health')
health() {
return { status: 'ok' };
}
}
Context Sharing
Middleware can share data with handlers via setContext() and getContext().
Type-Safe Context
Define your context shape using module augmentation:
declare module '@zeltjs/core' {
interface RequestContextSchema {
user: { id: number; name: string };
}
}
Setting Context in Middleware
@Middleware
export class AuthMiddleware {
async use(next: Next, req = request()): Promise<Response | undefined> {
const token = req.header('Authorization');
const user = await verifyToken(token);
setContext('user', user);
await next();
return undefined;
}
}
Reading Context in Handlers
@Controller('/profile')
export class ProfileController {
@Get('/')
getProfile(user = getContext('user')) {
return { id: user?.id, name: user?.name };
}
}
Dependency Injection
For middleware that requires dependency injection, use @Middleware:
import { Config, Env, Middleware, inject, request } from '@zeltjs/core';
import type { Next } from '@zeltjs/core';
@Config
class AuthConfig {
static readonly Token = AuthConfig;
constructor(private env = inject(Env)) {}
get secret() {
return this.env.getString('AUTH_SECRET');
}
}
@Middleware
export class AuthMiddleware {
constructor(private config = inject(AuthConfig)) {}
async use(next: Next, req = request()): Promise<Response | undefined> {
const secret = this.config.secret;
// ... authentication logic
await next();
return undefined;
}
}
Use class middleware the same way as function middleware:
@UseMiddleware(AuthMiddleware)
@Controller('/admin')
export class AdminController {
@Get('/') index() { return { ok: true }; }
}
Parameterized Middleware
For middleware that requires configuration options, pass options as the second @UseMiddleware() argument:
@Middleware
export class RateLimitMiddleware {
async use(next: Next, options: { limit: number; windowSec: number }) {
const { limit, windowSec } = options;
// ... rate limiting logic
await next();
return undefined;
}
}
@Controller('/api')
export class ApiController {
@UseMiddleware(RateLimitMiddleware, { limit: 10, windowSec: 60 })
@Post('/submit')
submit() {
return { submitted: true };
}
}
The options parameter is passed to the middleware's use() method at runtime.
Request Flow
Request
↓
Global Middleware (before next)
↓
Controller Middleware (before next)
↓
Method Middleware (before next)
↓
Route Handler
↓
Method Middleware (after next)
↓
Controller Middleware (after next)
↓
Global Middleware (after next)
↓
Response
Middleware can process both before and after the route handler by placing logic before or after await next().
Execution Order
Middleware executes in this order:
- Global middleware (in array order)
- Controller middleware (in decorator order)
- Method middleware (in decorator order)
- Route handler
- Post-handler middleware (reverse order after
next())
@Middleware
class GlobalMiddleware {
async use(next: Next) {
console.log('1. global before');
await next();
console.log('6. global after');
}
}
@Middleware
class ControllerMiddleware {
async use(next: Next) {
console.log('2. controller before');
await next();
console.log('5. controller after');
}
}
@Middleware
class MethodMiddleware {
async use(next: Next) {
console.log('3. method before');
await next();
console.log('4. method after');
}
}
Common Patterns
Middleware is written as classes. Use request(), response(), setContext(), and getContext() for framework primitives.
Restrict Access
Use class middleware when you need to inject services:
@Middleware
export class RequireAdmin {
constructor(private authService = inject(AuthService)) {}
async use(next: Next): Promise<Response | undefined> {
const user = currentUser();
if (!this.authService.isAdmin(user)) {
return Response.json({ error: 'Forbidden' }, { status: 403 });
}
await next();
return undefined;
}
}
Add Response Headers
Use response() for response headers:
@Middleware
class PoweredByMiddleware {
async use(next: Next, res = response()) {
res.header('X-Powered-By', 'zelt');
await next();
}
}
Measure Response Time
@Middleware
class TimingMiddleware {
async use(next: Next, res = response()) {
const start = Date.now();
await next();
res.header('X-Response-Time', `${Date.now() - start}ms`);
}
}