NestJS in Production · Structure, Validation and Errors
Interceptors: Logging, Response Shaping and Timeouts
Wrap handlers with interceptors to measure timing, shape responses consistently, and fail slow requests with a timeout.
What you'll build
Three interceptors: one that logs method, path and duration; one that wraps every successful response as { data: ... }; and one that turns handlers slower than 200 ms into a 504.
The idea
- An interceptor wraps the handler: code before
next.handle()runs first, and RxJS operators on the returned observable act on the result (map,tap,timeout,catchError). - Use them for cross-cutting concerns that need the result: logging, metrics, caching, response envelopes, timeouts.
- Order matters: globally registered interceptors wrap controller-level ones.
The code
import "reflect-metadata";
import { CallHandler, Controller, ExecutionContext, GatewayTimeoutException, Get, Injectable, Module, NestInterceptor } from "@nestjs/common";
import { NestFactory } from "@nestjs/core";
import { Observable, TimeoutError, catchError, map, tap, throwError, timeout } from "rxjs";
const log: string[] = [];
@Injectable()
class TimingInterceptor implements NestInterceptor {
intercept(ctx: ExecutionContext, next: CallHandler): Observable<unknown> {
const req = ctx.switchToHttp().getRequest();
const start = Date.now();
return next.handle().pipe(tap({
next: () => log.push(`${req.method} ${req.url} ok ${Date.now() - start < 100 ? "<100ms" : ">=100ms"}`),
error: (e) => log.push(`${req.method} ${req.url} failed: ${e.constructor.name}`),
}));
}
}
@Injectable()
class EnvelopeInterceptor implements NestInterceptor {
intercept(_: ExecutionContext, next: CallHandler) { return next.handle().pipe(map((data) => ({ data }))); }
}
@Injectable()
class TimeoutInterceptor implements NestInterceptor {
intercept(_: ExecutionContext, next: CallHandler) {
return next.handle().pipe(timeout(200), catchError((e) => throwError(() => e instanceof TimeoutError ? new GatewayTimeoutException("upstream too slow") : e)));
}
}
@Controller()
class AppController {
@Get("fast") fast() { return { items: [1, 2] }; }
@Get("slow") async slow() { await new Promise((r) => setTimeout(r, 500)); return { never: "seen" }; }
}
@Module({ controllers: [AppController] })
class AppModule {}
async function main() {
const app = await NestFactory.create(AppModule, { logger: false });
app.useGlobalInterceptors(new TimingInterceptor(), new EnvelopeInterceptor(), new TimeoutInterceptor());
await app.listen(0, "127.0.0.1");
const base = `http://127.0.0.1:${app.getHttpServer().address().port}`;
for (const path of ["/fast", "/slow"]) {
const r = await fetch(base + path);
console.log(path, r.status, JSON.stringify(await r.json()));
}
await app.close();
console.log(log.join("\n"));
}
main();
Output
/fast 200 {"data":{"items":[1,2]}}
/slow 504 {"message":"upstream too slow","error":"Gateway Timeout","statusCode":504}
GET /fast ok <100ms
GET /slow failed: GatewayTimeoutExceptionRun this on your computer — see “Get Started” for setup.
Common mistakes
- Doing response shaping in every controller instead of once in an interceptor.
- Timeouts that abort the HTTP response but not the underlying work (database query still running); pass cancellation down where possible.
- Logging full request bodies (personal data) in a generic interceptor.
Key points
- Interceptors wrap handlers and can act before and after them.
- Use RxJS operators for timing, mapping and timeouts.
- Keep cross-cutting behaviour in one place, registered globally.
Further reading: Interceptors · Request lifecycle
Practice questions
What can an interceptor do that a guard or middleware can't?
Answer
It wraps the handler's result stream, so it can transform or time the response, handle errors and apply timeouts after the handler runs.
How do you add a timeout to every request in NestJS?
Answer
A global interceptor applying the RxJS timeout operator and mapping TimeoutError to a 504 (or 408) exception.
In which order do global and controller interceptors run?
Answer
Global interceptors wrap controller-level ones, which wrap route-level ones; the outermost sees the request first and the response last.
Open in the app to mark lessons complete, take notes and track your progress.