Execution Context
NestJS runs the same enhancer abstractions — guards, interceptors, exception filters, and pipes — across multiple transports: HTTP servers, microservices (RPC), and WebSocket gateways. To make those enhancers reusable, Nest wraps the underlying request arguments in two helper objects, ArgumentsHost and ExecutionContext. These give you a transport-agnostic handle on the current request, plus enough metadata to branch on the active transport when you genuinely need to. Understanding them is the key to writing enhancers that work everywhere.
ArgumentsHost: the raw arguments wrapper
Every enhancer receives a way to reach the arguments that were originally passed to the handler. For an HTTP request that means [request, response, next]; for RPC it is [data, context]; for WebSockets it is [client, data]. ArgumentsHost abstracts over these shapes so you do not hard-code an array index.
The most important methods are getType(), which tells you the current transport, and the switchTo* methods, which return a strongly typed accessor for that transport.
import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common';
import { Request, Response } from 'express';
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = 500;
response.status(status).json({
statusCode: status,
path: request.url,
timestamp: new Date().toISOString(),
});
}
}
Here switchToHttp() returns an HttpArgumentsHost whose getResponse() and getRequest() are typed to your platform (Express or Fastify). If you call the wrong switchTo* for the active transport, the accessors return values that do not match the underlying objects, so always branch on getType() first when a filter must serve more than one transport.
ExecutionContext: ArgumentsHost plus handler metadata
ExecutionContext extends ArgumentsHost, so it has every method above and adds two more that describe which handler is about to run:
| Method | Returns | Typical use |
|---|---|---|
getClass() | The controller (or provider) class | Read class-level metadata |
getHandler() | A reference to the route handler method | Read method-level metadata |
getType() | The transport string ('http', 'rpc', 'ws', or a custom type) | Branch on transport |
switchToHttp() | HttpArgumentsHost | Access request/response |
switchToRpc() | RpcArgumentsHost | Access the RPC payload |
switchToWs() | WsArgumentsHost | Access the socket client |
getClass() and getHandler() are what make metadata-driven enhancers possible. Guards and interceptors receive an ExecutionContext precisely because they often need to read decorator metadata attached to the target route.
Reading metadata with Reflector
The canonical pattern is a roles guard. A @Roles() decorator attaches metadata to a handler, and the guard reads it back through the Reflector helper, using getHandler() and getClass() as the metadata targets.
import { SetMetadata } from '@nestjs/common';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from './roles.decorator';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const required = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
if (!required) return true;
const { user } = context.switchToHttp().getRequest();
return required.some((role) => user?.roles?.includes(role));
}
}
getAllAndOverride checks the handler first, then falls back to the class, which is exactly how you want method-level decorators to override controller-level defaults.
Tip: Prefer
reflector.getAllAndOverrideandgetAllAndMergeover the olderreflector.get. They accept the[handler, class]target array directly and express override/merge semantics without manual plumbing.
Switching on context type for transport-agnostic code
When a single enhancer is bound globally it may run for HTTP, RPC, and WS requests. Use getType() to extract the right request object for each.
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class TimingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
const start = Date.now();
const label = this.describe(context);
return next
.handle()
.pipe(tap(() => console.log(`${label} took ${Date.now() - start}ms`)));
}
private describe(context: ExecutionContext): string {
switch (context.getType()) {
case 'http':
return context.switchToHttp().getRequest().url;
case 'rpc':
return `rpc:${JSON.stringify(context.switchToRpc().getData())}`;
case 'ws':
return `ws:${context.switchToWs().getClient().id}`;
default:
return 'unknown';
}
}
}
Output:
/users took 14ms
rpc:{"id":42} took 3ms
ws:abx91 took 7ms
If you use GraphQL, the context type can be narrowed with a generic so TypeScript knows about the extra 'graphql' value:
import { GqlExecutionContext } from '@nestjs/graphql';
if (context.getType<'http' | 'rpc' | 'ws' | 'graphql'>() === 'graphql') {
const gqlCtx = GqlExecutionContext.create(context);
const request = gqlCtx.getContext().req;
}
GqlExecutionContext.create() wraps the standard ExecutionContext and remaps getArgs() to GraphQL’s (root, args, ctx, info) tuple, which is why a plain switchToHttp() does not work directly inside a resolver enhancer.
Best practices
- Always call the
switchTo*method that matches the current transport; guard multi-transport enhancers withgetType()first. - Type the
getRequest<T>()andgetResponse<T>()calls with your platform’s types (expressorfastify) for safe property access. - Read metadata through
Reflector.getAllAndOverride/getAllAndMergewith[getHandler(), getClass()]so method-level decorators override class-level ones. - Keep enhancers stateless and lean — the context object lives only for the current request, so do not cache request data on the instance.
- Use
GqlExecutionContext.create(context)inside GraphQL enhancers rather than reaching into the raw arguments. - Bind broadly reusable enhancers globally and let
getType()handle per-transport differences, instead of writing one enhancer per transport.