Skip to content
NestJS ns fundamentals 4 min read

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:

MethodReturnsTypical use
getClass()The controller (or provider) classRead class-level metadata
getHandler()A reference to the route handler methodRead method-level metadata
getType()The transport string ('http', 'rpc', 'ws', or a custom type)Branch on transport
switchToHttp()HttpArgumentsHostAccess request/response
switchToRpc()RpcArgumentsHostAccess the RPC payload
switchToWs()WsArgumentsHostAccess 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.getAllAndOverride and getAllAndMerge over the older reflector.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 with getType() first.
  • Type the getRequest<T>() and getResponse<T>() calls with your platform’s types (express or fastify) for safe property access.
  • Read metadata through Reflector.getAllAndOverride/getAllAndMerge with [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.
Last updated June 14, 2026
Was this helpful?