How to Skip a Global NestJS Interceptor for Specific Routes
Skip a global NestJS response interceptor on selected routes with SetMetadata and Reflector, while keeping response wrapping enabled everywhere else.
Use a custom decorator and NestJS Reflector metadata to skip a global interceptor for one route or an entire controller. This pattern works well when most API responses use one JSON wrapper but webhooks, file downloads, streams, or proxy endpoints must return their original response.
Chinese version of this article
Mark routes that skip the interceptor
Define a metadata key and expose it through a decorator:
import { SetMetadata } from '@nestjs/common';
export const SKIP_RESPONSE_WRAP = 'skipResponseWrap';
export const SkipResponseWrap = () =>
SetMetadata(SKIP_RESPONSE_WRAP, true);
Apply @SkipResponseWrap() to a route that must return its original value:
import { Body, Controller, HttpCode, Post } from '@nestjs/common';
import { SkipResponseWrap } from './skip-response-wrap.decorator';
@Controller('webhooks')
export class WebhookController {
@Post('provider')
@HttpCode(200)
@SkipResponseWrap()
handleWebhook(@Body() body: unknown) {
return 'success';
}
}
Place the decorator on the controller class when every route in that controller should skip wrapping.
Read skip metadata in the global interceptor
Start with the interceptor imports and response type:
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { map } from 'rxjs/operators';
import { SKIP_RESPONSE_WRAP } from './skip-response-wrap.decorator';
interface ApiResponse<T> {
code: number;
success: true;
data: T;
}
Then inject Reflector and check metadata before applying the response mapping:
@Injectable()
export class ResponseWrapInterceptor<T>
implements NestInterceptor<T, ApiResponse<T> | T>
{
constructor(private readonly reflector: Reflector) {}
intercept(context: ExecutionContext, next: CallHandler<T>) {
const skip = this.reflector.getAllAndOverride<boolean>(
SKIP_RESPONSE_WRAP,
[context.getHandler(), context.getClass()],
);
if (skip) return next.handle();
return next.handle().pipe(
map((data) => ({ code: 0, success: true, data })),
);
}
}
context.getHandler() reads route-level metadata. context.getClass() reads controller-level metadata. getAllAndOverride() lets the route setting take precedence over the controller setting.
Register the interceptor with dependency injection
Register the interceptor through APP_INTERCEPTOR so NestJS can inject Reflector:
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { ResponseWrapInterceptor } from './response-wrap.interceptor';
@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: ResponseWrapInterceptor,
},
],
})
export class AppModule {}
Most controllers can now return business data directly. The interceptor converts successful results into a shared shape:
{
"code": 0,
"success": true,
"data": {
"user": "example_user",
"imageUrl": "https://example.com/avatar.png"
}
}
The decorated webhook route returns its required plain-text value instead:
success
Keep error responses in exception filters
Some implementations use another interceptor with catchError() to wrap errors. That can work, but it is not always the clearest boundary.
Wrapping successful responses is a transformation after a handler returns normally, which fits an interceptor well. Error responses are exception handling, and NestJS has exception filters for that. Keeping the two paths separate reduces the chance of swallowing exceptions inside a response interceptor.
A simplified HTTP exception filter can look like this:
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
HttpStatus,
} from '@nestjs/common';
import { Response } from 'express';
@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const status =
exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const message =
exception instanceof HttpException
? exception.message
: 'Internal server error';
response.status(status).json({
code: status,
success: false,
message,
});
}
}
Register it globally in main.ts:
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new HttpExceptionFilter());
await app.listen(3000);
Real projects can add business error codes, request IDs, logging, and custom exception classes. The core boundary stays the same: use an interceptor for successful response mapping, and use a filter for exception responses.
Decide which endpoints should skip wrapping
A global interceptor affects every endpoint. Some endpoints should not return a JSON wrapper:
- Webhooks from WeChat, GitHub, Stripe, and similar platforms may require a fixed body or status code.
- File download endpoints need to return streams.
- Images, QR codes, and CSV exports have their own
Content-Type. - Proxy endpoints may need to pass through the upstream response.
If the interceptor wraps one of these responses as { code, success, data }, the caller may reject it. Mark that endpoint explicitly instead of adding response-type guesses to the interceptor.
Account for response mapping boundaries
The NestJS documentation includes an important warning: response mapping does not work with the library-specific response strategy, such as directly using the @Res() object in a handler.
A practical split is:
- Normal JSON APIs: return business objects and let the global interceptor wrap them.
- Protocol-specific responses: add
@SkipResponseWrap()and return the exact value required. - File streams or strongly controlled headers: add
@SkipResponseWrap()and use@Res()orStreamableFilewhen needed. - Error responses: prefer exception filters instead of mixing them into the successful response interceptor.
This keeps the global rule simple while still giving special endpoints an explicit escape hatch. The interceptor wraps successful responses, the decorator declares exceptions, and the exception filter handles error shape.
Loading discussion...
Discussion failed to load. Reload