Skip to content
NestJS ns graphql 5 min read

GraphQL Guards & Context

Guards are NestJS’s primary tool for authorization, but they were designed around the HTTP request/response cycle. GraphQL doesn’t expose req and res the same way — a single HTTP request can resolve many fields, and the execution context wraps the request inside a different argument shape. To reuse your existing guards in resolvers you adapt the ExecutionContext with GqlExecutionContext, pull the underlying request out of the GraphQL context, and inject the authenticated user into your resolvers. This page shows how to wire that up end to end.

How the GraphQL execution context differs

For HTTP handlers, context.switchToHttp().getRequest() returns the Express/Fastify request. In GraphQL, a resolver is invoked with four arguments — root, args, context, and info — and NestJS packs all of them into the ExecutionContext. The @nestjs/graphql package gives you GqlExecutionContext.create(context) to read those arguments back out in a typed way.

The shared GraphQL context object is built once per request by Apollo and passed to every resolver. By default it contains the req (and res for non-subscription transports), which is where Passport and most auth middleware stash the authenticated user.

import { ExecutionContext, Injectable } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class GqlAuthGuard extends AuthGuard('jwt') {
  getRequest(context: ExecutionContext) {
    const ctx = GqlExecutionContext.create(context);
    return ctx.getContext().req;
  }
}

Extending AuthGuard('jwt') and overriding only getRequest is the cleanest way to reuse Passport. The base guard’s canActivate calls getRequest, so redirecting it to the GraphQL request is all the adaptation you need.

Exposing the request in the GraphQL context

The guard above assumes ctx.getContext().req exists. That only happens if you forward the request when configuring the driver. With Apollo this is the default for queries and mutations, but it is worth being explicit:

import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { join } from 'path';

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
      autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
      sortSchema: true,
      context: ({ req }) => ({ req }),
    }),
  ],
})
export class AppModule {}

The context factory receives the transport payload and returns the object handed to every resolver. Returning { req } guarantees getContext().req is populated.

Securing queries and mutations

With the guard in place, protect resolvers exactly as you would HTTP routes — apply @UseGuards at the method or resolver-class level.

import { UseGuards } from '@nestjs/common';
import { Resolver, Query, Mutation, Args } from '@nestjs/graphql';
import { GqlAuthGuard } from './gql-auth.guard';
import { ProjectsService } from './projects.service';
import { Project } from './models/project.model';

@Resolver(() => Project)
export class ProjectsResolver {
  constructor(private readonly projects: ProjectsService) {}

  @Query(() => [Project])
  @UseGuards(GqlAuthGuard)
  myProjects() {
    return this.projects.findAll();
  }

  @Mutation(() => Project)
  @UseGuards(GqlAuthGuard)
  createProject(@Args('name') name: string) {
    return this.projects.create(name);
  }
}

If an unauthenticated client calls a guarded operation, NestJS throws an UnauthorizedException, which Apollo serialises into the errors array.

Output:

{
  "errors": [
    {
      "message": "Unauthorized",
      "extensions": { "code": "UNAUTHORIZED" }
    }
  ],
  "data": null
}

Injecting the authenticated user

Once a guard has validated the request, the user lives on req.user. Reading GqlExecutionContext in every resolver is noisy, so build a custom param decorator that does it once.

import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';

export const CurrentUser = createParamDecorator(
  (data: keyof UserEntity | undefined, context: ExecutionContext) => {
    const ctx = GqlExecutionContext.create(context);
    const user = ctx.getContext().req.user as UserEntity;
    return data ? user?.[data] : user;
  },
);

Now resolvers receive the user as a clean, typed argument:

@Query(() => [Project])
@UseGuards(GqlAuthGuard)
myProjects(@CurrentUser() user: UserEntity) {
  return this.projects.findByOwner(user.id);
}

You can also accept the optional data argument to grab a single field, e.g. @CurrentUser('email') email: string.

Reading args, root, and info from the context

GqlExecutionContext exposes every resolver argument, which is useful for guards that make decisions based on the requested data — for instance an ownership or rate-limiting guard.

MethodReturnsTypical use
getContext()Shared per-request context ({ req })Read the authenticated user / headers
getArgs()Resolver arguments objectInspect requested IDs before allowing access
getRoot()Parent objectField-level guards on nested types
getInfo()GraphQL info (AST, field name)Audit which field is being resolved
@Injectable()
export class ProjectOwnerGuard implements CanActivate {
  constructor(private readonly projects: ProjectsService) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const ctx = GqlExecutionContext.create(context);
    const { id } = ctx.getArgs<{ id: string }>();
    const user = ctx.getContext().req.user as UserEntity;
    const project = await this.projects.findOne(id);
    return project?.ownerId === user.id;
  }
}

Tip: Stack guards in order — put GqlAuthGuard before ProjectOwnerGuard so the user is guaranteed to exist when the ownership check runs: @UseGuards(GqlAuthGuard, ProjectOwnerGuard).

Warning: Subscriptions use the WebSocket transport, where req is not present. Guards relying on getContext().req will fail there. For subscriptions, validate the connection in onConnect / subscriptions.onConnect and read the user from the connection context instead.

Best practices

  • Adapt guards by overriding getRequest rather than reimplementing canActivate — you inherit all of Passport’s behaviour for free.
  • Always return { req } from the GraphQL context factory so guards and decorators have a stable place to find the request.
  • Centralise user extraction in a @CurrentUser() param decorator instead of calling GqlExecutionContext in every resolver.
  • Compose fine-grained guards (auth, then ownership, then role) and let NestJS run them in declaration order.
  • Keep authorization in guards and field-level redaction in field resolvers — don’t leak unauthorized data by resolving it first and filtering later.
  • Remember the request resolves the whole operation: a guard on the root query won’t re-run for nested field resolvers, so guard those fields explicitly when needed.
Last updated June 14, 2026
Was this helpful?