title: 'Interceptors - NL Framework' description: 'Learn how to extend request/response behavior with interceptors in NL Framework.'

Runtime

Interceptors

Interceptors wrap the execution of your route handlers, giving you a single place to run code before and after controllers execute. Use them for logging, response mapping, caching, exception translation, or any cross-cutting behavior.

Key pipeline order: middleware ➜ guards ➜ interceptors ➜ pipes ➜ handler ➜ filters. Interceptors share the same HttpExecutionContext as guards and can short-circuit requests by returning their own Response objects.

Implementing an interceptor

Interceptors implement the HttpInterceptor interface. They receive the execution context and a CallHandler that invokes the next interceptor (or ultimately the route handler).

Logging interceptor
import { Injectable } from '@nl-framework/core';import { CallHandler, HttpInterceptor, HttpExecutionContext } from '@nl-framework/http';@Injectable()export class LoggingInterceptor implements HttpInterceptor {  async intercept(context: HttpExecutionContext, next: CallHandler) {    const start = Date.now();    console.log('Before handler', context.getRoute().handlerName);    const result = await next.handle();    console.log('After handler', Date.now() - start, 'ms');    return result;  }}

Binding interceptors

Use the @UseInterceptors() decorator to attach interceptors to controllers or individual handlers. Order matters—the first decorator argument wraps everything that follows. Import the decorator from @nl-framework/core (each transport re-exports it for backwards compatibility) so HTTP, GraphQL, and microservice handlers share the exact same metadata.

Controller and method scopes
import { Controller, Get } from '@nl-framework/http';import { UseInterceptors } from '@nl-framework/core';@Controller('/reports')@UseInterceptors(LoggingInterceptor)export class ReportsController {  @Get()  async list() {    return { reports: [] };  }  @Get('/daily')  @UseInterceptors(RequestTimingInterceptor)  daily() {    return { status: 'ok' };  }}

Functional interceptors

For lightweight cases you can pass an InterceptorFunction. Functional interceptors are great for caching or analytics because they require no class registration.

Cache interceptor
import { InterceptorFunction } from '@nl-framework/http';export const CacheInterceptor: InterceptorFunction = async (context, next) => {  const cacheKey = context.getRoute().controller?.name + ':' + context.getRequest().url;  const cached = await context.getContainer().resolve(CacheStore).get(cacheKey);  if (cached) {    return new Response(JSON.stringify(cached), {      headers: { 'content-type': 'application/json' },    });  }  const result = await next.handle();  await context.getContainer().resolve(CacheStore).set(cacheKey, result);  return result;};@Controller('/inventory')export class InventoryController {  @Get()  @UseInterceptors(CacheInterceptor)  list() {    return this.service.all();  }}

Global interceptors

Call registerHttpInterceptor() during bootstrap to run an interceptor for every request. Global interceptors execute outside controller scopes, so prefer functional interceptors or stateless classes.

Register a global interceptor
import { registerHttpInterceptor } from '@nl-framework/http';registerHttpInterceptor(LoggingInterceptor);const app = await createHttpApplication(AppModule, { port: 3000 });await app.listen();

GraphQL resolvers

The same @UseInterceptors() decorator now wraps GraphQL resolvers. Combine controller-scoped and global registerGraphqlInterceptor() hooks to envelope responses, cache resolver results, or run observability spans without duplicating logic in HTTP land.

Resolver interceptors
import { ObjectType, Field } from '@nl-framework/graphql';import { Resolver, Query } from '@nl-framework/graphql';import { UseInterceptors } from '@nl-framework/core';import {  GraphqlCallHandler,  GraphqlInterceptor,  registerGraphqlInterceptor,} from '@nl-framework/graphql';class EnvelopeInterceptor implements GraphqlInterceptor {  async intercept(_context: unknown, next: GraphqlCallHandler) {    const result = await next.handle();    return { data: result };  }}registerGraphqlInterceptor(EnvelopeInterceptor);@ObjectType()class Report {  @Field()  message!: string;}@Resolver(() => Report)@UseInterceptors(EnvelopeInterceptor)export class ReportResolver {  @Query(() => Report)  stats() {    return { message: 'report' };  }}

Transforming responses

Wrap handler results before they leave the server. This is helpful for enforcing envelope shapes, stripping null values, or appending metadata.

Response envelopes
import { CallHandler, HttpExecutionContext, HttpInterceptor } from '@nl-framework/http';export class EnvelopeInterceptor implements HttpInterceptor {  async intercept(_context: HttpExecutionContext, next: CallHandler) {    const result = await next.handle();    return { data: result, at: new Date().toISOString() };  }}@Controller('/users')@UseInterceptors(EnvelopeInterceptor)export class UsersController {  @Get()  list() {    return [{ id: 1, email: 'ada@example.com' }];  }}

Translating exceptions

Because interceptors surround the handler, they can catch exceptions and transform them into framework-friendly responses. This is a good place to unify error payloads across HTTP and GraphQL transports.

Normalize framework errors
import { HttpInterceptor, CallHandler, HttpExecutionContext } from '@nl-framework/http';import { ApplicationException } from '@nl-framework/core';export class ErrorsInterceptor implements HttpInterceptor {  async intercept(_context: HttpExecutionContext, next: CallHandler) {    try {      return await next.handle();    } catch (error) {      if (error instanceof ApplicationException) {        return new Response(          JSON.stringify({ code: error.code, message: error.message }),          { status: 502, headers: { 'content-type': 'application/json' } },        );      }      throw error;    }  }}

Short-circuiting or timing out

Interceptors can completely bypass the handler. Return a cached Response, or race the handler against a timeout promise and throw when it takes too long.

Timeout interceptor
import { HttpInterceptor, HttpExecutionContext, CallHandler } from '@nl-framework/http';export class TimeoutInterceptor implements HttpInterceptor {  constructor(private readonly timeoutMs = 5000) {}  async intercept(_context: HttpExecutionContext, next: CallHandler) {    const controller = new AbortController();    const timeout = setTimeout(() => controller.abort(), this.timeoutMs);    try {      return await Promise.race([        next.handle(),        new Promise((_, reject) => controller.signal.addEventListener('abort', () => reject(new Error('Request timeout')))),      ]);    } finally {      clearTimeout(timeout);    }  }}

Best practices

Be explicit about order. Declare interceptors from most global to most specific to avoid surprises.
Keep them stateless when global. If an interceptor needs dependencies, register it via DI so the container can manage scope.
Return consistent shapes. When transforming responses, wrap data predictably so clients know what to expect.
Short-circuit intentionally. Always document when an interceptor might skip the underlying handler (caching, feature flags, etc.).