Interceptors
Interceptors
Section titled “Interceptors”Interceptors wrap route handlers in an onion-style pipeline — before and after the handler — without touching business logic. Use them for logging, caching, response transformation, timing, and more.
The Interceptor interface
Section titled “The Interceptor interface”import type { Interceptor, ExecutionContext } from 'veloce-ts';
class LoggingInterceptor implements Interceptor { async intercept( ctx: ExecutionContext, next: () => Promise<Response> ): Promise<Response> { const start = Date.now(); const response = await next(); console.log(`${ctx.request.method} ${ctx.request.url} — ${Date.now() - start}ms`); return response; }}The next() call runs the remaining interceptors and then the route handler. Everything before next() is “before” logic; everything after is “after” logic.
ExecutionContext
Section titled “ExecutionContext”type ExecutionContext = { request: Request; // Web standard Request controller: unknown; // controller instance (may be undefined for functional routes) handler: string; // method name on the controller};Registering globally
Section titled “Registering globally”const app = new Veloce();app.useInterceptor(new LoggingInterceptor());app.useInterceptor(new MetricsInterceptor());await app.compile();Global interceptors run for every route, before class/method interceptors.
@UseInterceptor — per method or class
Section titled “@UseInterceptor — per method or class”import { UseInterceptor } from 'veloce-ts';
@Controller('/users')@UseInterceptor(LoggingInterceptor) // applies to all methods in the classclass UserController { @Get('/') @UseInterceptor(CacheInterceptor) // stacks on top of class-level async getUsers() { return users; }}Execution order
Section titled “Execution order”Global interceptors (registration order) → Class interceptors (decorator order) → Method interceptors (decorator order) → Route handler ← Method interceptors (reverse) ← Class interceptors (reverse)← Global interceptors (reverse)Examples
Section titled “Examples”Timing interceptor
Section titled “Timing interceptor”class TimingInterceptor implements Interceptor { async intercept(ctx: ExecutionContext, next: () => Promise<Response>) { const start = performance.now(); const response = await next(); const ms = (performance.now() - start).toFixed(2); // Clone to add header (Response is immutable) const modified = new Response(response.body, response); modified.headers.set('X-Response-Time', `${ms}ms`); return modified; }}Simple cache interceptor
Section titled “Simple cache interceptor”const cache = new Map<string, Response>();
class CacheInterceptor implements Interceptor { async intercept(ctx: ExecutionContext, next: () => Promise<Response>) { const key = ctx.request.url; if (cache.has(key)) { return cache.get(key)!.clone(); } const response = await next(); if (response.ok) { cache.set(key, response.clone()); } return response; }}Auth audit interceptor
Section titled “Auth audit interceptor”class AuditInterceptor implements Interceptor { async intercept(ctx: ExecutionContext, next: () => Promise<Response>) { const response = await next(); if (response.status === 401 || response.status === 403) { console.warn(`Unauthorized access attempt: ${ctx.request.url}`); } return response; }}Transform response shape
Section titled “Transform response shape”class WrapResponseInterceptor implements Interceptor { async intercept(ctx: ExecutionContext, next: () => Promise<Response>) { const response = await next(); if (!response.headers.get('content-type')?.includes('json')) { return response; } const data = await response.json(); return Response.json({ data, timestamp: Date.now() }, response); }}Best practices
Section titled “Best practices”- Keep interceptors stateless — inject state via constructor or closure.
- Always call
next()unless you intend to short-circuit the pipeline. - Return a cloned Response when adding headers —
Responseis immutable. - Register frequently-needed interceptors globally; feature-specific ones with
@UseInterceptor.