title: 'Middleware · Nael Framework' description: 'Learn how to use middleware for request processing, logging, authentication, and error handling in Nael applications.'
Middleware
Middleware is a function which is called before the route handler. Middleware functions have access to the request context and can perform tasks like logging, authentication, request transformation, and error handling. They execute in the order they are registered.
Middleware function
Middleware functions in Nael are defined using the MiddlewareHandler type. Each middleware receives
the request context and a next() function to call the next middleware in the chain or the final route handler.
import { MiddlewareHandler } from '@nl-framework/http';const loggingMiddleware: MiddlewareHandler = async (ctx, next) => { console.log(`[${ctx.request.method}] ${new URL(ctx.request.url).pathname}`); const response = await next(); console.log(`Response status: ${response.status}`); return response;};The middleware must call await next() to pass control to the next middleware or route handler, and it must
return a Response object. You can modify the request context before calling next(), or modify
the response after calling next().
Applying middleware
You can apply middleware to your HTTP application in two ways: by passing them during application creation,
or by calling use() on the HTTP application instance.
import { NaelFactory } from '@nl-framework/platform';const app = await NaelFactory.create(AppModule);const httpApp = app.getHttpApplication();if (httpApp) { httpApp.use(loggingMiddleware); httpApp.use(requestTimingMiddleware);}await app.listen({ http: 3000 });Alternatively, you can pass middleware during application creation:
import { createHttpApplication } from '@nl-framework/http';const app = await createHttpApplication(AppModule, { port: 3000, middleware: [ loggingMiddleware, requestTimingMiddleware, ],});await app.listen();Request timing
A common use case for middleware is to measure request duration. Here's an example that logs the time taken to process each request:
import { MiddlewareHandler } from '@nl-framework/http';const requestTimingMiddleware: MiddlewareHandler = async (ctx, next) => { const started = Date.now(); try { const response = await next(); const elapsed = Date.now() - started; console.log('Request completed', { method: ctx.request.method, path: new URL(ctx.request.url).pathname, status: response.status, elapsedMs: elapsed, }); return response; } catch (error) { const elapsed = Date.now() - started; console.error('Request failed', { method: ctx.request.method, path: new URL(ctx.request.url).pathname, elapsedMs: elapsed, error, }); throw error; }};CORS middleware
Enable Cross-Origin Resource Sharing (CORS) by adding appropriate headers to responses:
import { MiddlewareHandler } from '@nl-framework/http';const corsMiddleware: MiddlewareHandler = async (ctx, next) => { // Handle preflight requests if (ctx.request.method === 'OPTIONS') { return new Response(null, { status: 204, headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, Authorization', 'Access-Control-Max-Age': '86400', }, }); } const response = await next(); // Add CORS headers to the response const headers = new Headers(response.headers); headers.set('Access-Control-Allow-Origin', '*'); headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS'); headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization'); return new Response(response.body, { status: response.status, statusText: response.statusText, headers, });};Authentication middleware
Middleware can be used to validate authentication tokens and protect routes:
import { MiddlewareHandler } from '@nl-framework/http';const authMiddleware: MiddlewareHandler = async (ctx, next) => { const authHeader = ctx.request.headers.get('Authorization'); if (!authHeader || !authHeader.startsWith('Bearer ')) { return new Response( JSON.stringify({ error: 'Unauthorized' }), { status: 401, headers: { 'Content-Type': 'application/json' }, } ); } const token = authHeader.substring(7); try { // Validate token here const user = await validateToken(token); // Attach user to context (would need custom context extension) // For now, continue to next middleware return await next(); } catch (error) { return new Response( JSON.stringify({ error: 'Invalid token' }), { status: 401, headers: { 'Content-Type': 'application/json' }, } ); }};Note: For more sophisticated authentication scenarios, consider using Guards which provide better integration with the dependency injection system and can be applied at the controller or handler level.
Error handling
Middleware can catch and handle errors from downstream middleware and route handlers:
import { MiddlewareHandler } from '@nl-framework/http';const errorHandlingMiddleware: MiddlewareHandler = async (ctx, next) => { try { return await next(); } catch (error) { console.error('Unhandled error:', error); if (error instanceof ValidationError) { return new Response( JSON.stringify({ error: 'Validation failed', details: error.details, }), { status: 400, headers: { 'Content-Type': 'application/json' }, } ); } if (error instanceof NotFoundError) { return new Response( JSON.stringify({ error: 'Resource not found' }), { status: 404, headers: { 'Content-Type': 'application/json' }, } ); } return new Response( JSON.stringify({ error: 'Internal server error' }), { status: 500, headers: { 'Content-Type': 'application/json' }, } ); }};Error handling middleware should typically be registered first so it can catch errors from all other middleware and route handlers.
Request ID tracking
Add a unique identifier to each request for tracking and debugging:
import { MiddlewareHandler } from '@nl-framework/http';const requestIdMiddleware: MiddlewareHandler = async (ctx, next) => { const requestId = crypto.randomUUID(); const response = await next(); const headers = new Headers(response.headers); headers.set('X-Request-ID', requestId); return new Response(response.body, { status: response.status, statusText: response.statusText, headers, });};Response compression
Compress response bodies to reduce bandwidth usage:
import { MiddlewareHandler } from '@nl-framework/http';const compressionMiddleware: MiddlewareHandler = async (ctx, next) => { const response = await next(); const acceptEncoding = ctx.request.headers.get('Accept-Encoding') || ''; if (!acceptEncoding.includes('gzip')) { return response; } // Only compress JSON responses const contentType = response.headers.get('Content-Type') || ''; if (!contentType.includes('application/json')) { return response; } const body = await response.text(); const compressed = await Bun.gzipSync(Buffer.from(body)); const headers = new Headers(response.headers); headers.set('Content-Encoding', 'gzip'); headers.set('Content-Length', compressed.length.toString()); return new Response(compressed, { status: response.status, statusText: response.statusText, headers, });};Conditional middleware
You can apply middleware logic conditionally based on the request path or other criteria:
import { MiddlewareHandler } from '@nl-framework/http';const apiOnlyMiddleware: MiddlewareHandler = async (ctx, next) => { const url = new URL(ctx.request.url); // Only apply this middleware to /api routes if (!url.pathname.startsWith('/api')) { return await next(); } // Add API-specific headers const response = await next(); const headers = new Headers(response.headers); headers.set('X-API-Version', '1.0'); return new Response(response.body, { status: response.status, statusText: response.statusText, headers, });};Request context
Middleware functions receive a RequestContext object that provides access to request details,
route information, and the dependency injection container:
import { MiddlewareHandler } from '@nl-framework/http';const contextAwareMiddleware: MiddlewareHandler = async (ctx, next) => { // Access request details console.log('Method:', ctx.request.method); console.log('URL:', ctx.request.url); console.log('Headers:', Object.fromEntries(ctx.headers.entries())); console.log('Query params:', Object.fromEntries(ctx.query.entries())); console.log('Route params:', ctx.params); // Access route information console.log('Controller:', ctx.route.controller.name); console.log('Handler:', ctx.route.handlerName); console.log('Route path:', ctx.route.definition.path); // Access DI container const someService = await ctx.container.resolve(SomeService); return await next();};The context object includes:
request- The original Request objectparams- Route parameters extracted from the URL pathquery- URLSearchParams object for query string parametersheaders- Headers object for accessing request headersbody- Parsed request body (available after body parsing)route- Information about the matched route (controller, handler, definition)container- Access to the DI container for resolving services
Middleware order
The order in which middleware is registered matters. Middleware executes in the order it's registered, and the response flows back in reverse order. Here's a typical middleware stack:
import { NaelFactory } from '@nl-framework/platform';import { Logger, LoggerFactory } from '@nl-framework/logger';import type { MiddlewareHandler } from '@nl-framework/http';const bootstrap = async () => { const app = await NaelFactory.create(AppModule); const loggerFactory = await app.get<LoggerFactory>(LoggerFactory); const requestLogger = loggerFactory.create({ context: 'Request' }); const httpApp = app.getHttpApplication(); if (!httpApp) { throw new Error('HTTP application not available'); } // Error handling (first) httpApp.use(errorHandlingMiddleware); // Request logging const loggingMiddleware: MiddlewareHandler = async (ctx, next) => { const started = Date.now(); try { const response = await next(); requestLogger.debug('Request completed', { method: ctx.request.method, path: new URL(ctx.request.url).pathname, status: response.status, elapsedMs: Date.now() - started, }); return response; } catch (error) { requestLogger.error('Request failed', error); throw error; } }; httpApp.use(loggingMiddleware); // CORS httpApp.use(corsMiddleware); // Request ID httpApp.use(requestIdMiddleware); await app.listen({ http: 3000 });};bootstrap().catch(console.error);Best Practice: Register error handling middleware first so it can catch errors from all other middleware and route handlers. Register authentication and authorization middleware early in the chain, and register response modification middleware (like compression) later.
What's next?
Learn about Exception Filters for centralized error handling, or explore Guards for fine-grained access control.