title: 'Middleware · Nael Framework' description: 'Learn how to use middleware for request processing, logging, authentication, and error handling in Nael applications.'

Overview

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.

Basic middleware
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.

Using .use() method
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:

Passing middleware to createHttpApplication
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:

Request timing middleware
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:

CORS middleware
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:

Authentication middleware
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:

Error handling middleware
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:

Request ID middleware
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:

Compression middleware
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:

Conditional middleware
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:

Accessing request context
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 object
  • params - Route parameters extracted from the URL path
  • query - URLSearchParams object for query string parameters
  • headers - Headers object for accessing request headers
  • body - 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:

Complete middleware setup
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.