# Interceptors

> Source: https://learn-nestjs.com/interceptors/
> Part of Learn NestJS, free to read.

An interceptor wraps the route handler. It sees the request on the way in, the response on the way out, and — crucially — it runs even when the handler throws or the client disconnects.

```typescript src/common/logging.interceptor.ts
import { CallHandler, ExecutionContext, Injectable, NestInterceptor, Logger } from '@nestjs/common';
import { Observable, tap } from 'rxjs';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  private readonly logger = new Logger(LoggingInterceptor.name);

  intercept(ctx: ExecutionContext, next: CallHandler): Observable<unknown> {
    const req = ctx.switchToHttp().getRequest();
    const started = Date.now();

    return next.handle().pipe(
      tap({
        next: () => this.logger.log(`${req.method} ${req.url} ${Date.now() - started}ms`),
        error: (err) => this.logger.error(`${req.method} ${req.url} failed: ${err.message}`),
      }),
    );
  }
}
```

`next.handle()` returns an Observable of whatever the handler returns. Everything before that call runs **before** the handler; everything in the `.pipe()` runs **after**.

## Where interceptors sit

```text
middleware → guards → interceptors (before) → pipes → HANDLER
           → interceptors (after) → exception filters
```

That position is what makes them the right place for anything cross-cutting that needs to see the outcome. A guard can only say yes or no before the fact; an interceptor sees what happened.

## Registering

```typescript
@UseInterceptors(LoggingInterceptor)     // one handler or one controller
@Get()
findAll() {}
```

```typescript src/app.module.ts
providers: [{ provide: APP_INTERCEPTOR, useClass: LoggingInterceptor }]   // global, with DI
```

```typescript src/main.ts
app.useGlobalInterceptors(new LoggingInterceptor());   // global, no DI
```

Prefer `APP_INTERCEPTOR` for anything that needs injected dependencies — the `useGlobalInterceptors` form constructs the instance itself, so nothing can be injected into it.

## Transforming the response

```typescript
export interface Envelope<T> { data: T; timestamp: string }

@Injectable()
export class EnvelopeInterceptor<T> implements NestInterceptor<T, Envelope<T>> {
  intercept(_ctx: ExecutionContext, next: CallHandler<T>): Observable<Envelope<T>> {
    return next.handle().pipe(
      map((data) => ({ data, timestamp: new Date().toISOString() })),
    );
  }
}
```

Every handler now returns `{ data: ..., timestamp: ... }` without any handler knowing about it. Useful — and worth deciding once, early, because changing your response envelope later is a breaking change for every client.

## The serializer

The one interceptor you should almost always register globally:

```typescript
app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));
```

```typescript
export class UserEntity {
  id!: string;
  email!: string;

  @Exclude()
  passwordHash!: string;     // now stripped from every response
}
```

Without the interceptor, `@Exclude()` does nothing and your password hashes serialise. This pairing is a security control, not a formatting nicety — see [the security review](/review/security/).

## Timeouts

```typescript
@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
  intercept(_ctx: ExecutionContext, next: CallHandler): Observable<unknown> {
    return next.handle().pipe(
      timeout(5000),
      catchError((err) =>
        err instanceof TimeoutError
          ? throwError(() => new RequestTimeoutException())
          : throwError(() => err),
      ),
    );
  }
}
```

## Caching

```typescript
@Injectable()
export class CacheInterceptor implements NestInterceptor {
  constructor(@Inject(CACHE_MANAGER) private cache: Cache) {}

  async intercept(ctx: ExecutionContext, next: CallHandler): Promise<Observable<unknown>> {
    const req = ctx.switchToHttp().getRequest();
    if (req.method !== 'GET') return next.handle();

    const key = `${req.user?.id ?? 'anon'}:${req.url}`;   // include the user!
    const hit = await this.cache.get(key);
    if (hit !== undefined) return of(hit);

    return next.handle().pipe(tap((body) => this.cache.set(key, body, 30_000)));
  }
}
```

:::danger Never key a cache on the URL alone
Nest's built-in `CacheInterceptor` keys on the URL by default. On an authenticated endpoint like `/me`, that means the first user's response is served to everyone else. Always include the user in the key, or do not cache authenticated routes at all.
:::

## `finalize` — the operator worth knowing

`tap` fires on success or error. `finalize` also fires on **unsubscribe**, which is what happens when the client disconnects mid-response.

```typescript
return next.handle().pipe(
  finalize(() => {
    // runs on success, on error, AND on client disconnect
    this.metrics.record(feature, Date.now() - started);
  }),
);
```

If you are measuring anything — latency, cost, token usage — `finalize` is the operator that stops abandoned requests going unrecorded. That matters more than it sounds: abandoned work is exactly the category you most want visibility into.

## A minimal RxJS vocabulary

You do not need to learn RxJS to write interceptors. These five cover nearly everything:

```typescript
map(fn)                  // transform the response
tap({ next, error })     // side effect, no transformation
catchError(fn)           // handle an error
timeout(ms)              // fail if it takes too long
finalize(fn)             // cleanup, always runs
of(value)                // an Observable of one value — short-circuit the handler
```

## Exercise

```typescript
// Write a `RequestIdInterceptor` that:
//   - reads an `x-request-id` header, generating one if absent
//   - attaches it to the request object
//   - sets it on the response header
//   - logs method, url, request id and duration on completion,
//     including when the client disconnects
```

## Common questions

### Interceptor, guard or middleware?

Guard for "may this proceed" — it runs first and can reject cleanly. Middleware for anything that only needs the raw request and does not care about the route. Interceptor for anything that needs to see the *result*: logging, transforming, timing, caching.

### Do I need to learn RxJS?

Only the five operators above. Nest wraps the handler's return value in an Observable for you, and interceptors are the main place it surfaces. Nothing else in a typical Nest application requires it.

### Why is my global interceptor not getting its dependencies?

Because `useGlobalInterceptors` in `main.ts` constructs the instance outside the DI container. Register it as an `APP_INTERCEPTOR` provider in a module instead, and injection works normally.
