NestJS and GraphQL in 2026: Schemas, Resolvers and Interview Questions

Master NestJS GraphQL integration with schema-first and code-first approaches, resolver patterns, and common interview questions for senior developer positions.

NestJS GraphQL API architecture with resolvers and schema

NestJS GraphQL integration provides a structured approach to building type-safe APIs that leverage TypeScript decorators and GraphQL's query language. This tutorial covers both schema-first and code-first approaches, resolver patterns, and the interview questions hiring managers ask in 2026.

Code-First vs Schema-First

NestJS 11 defaults to code-first GraphQL, generating the schema from TypeScript classes. Schema-first remains available for teams with existing .graphql files or SDL-based workflows.

Setting Up NestJS GraphQL with Apollo Server

NestJS integrates with Apollo Server through the @nestjs/graphql package. The setup differs based on the chosen approach—code-first generates SDL from decorators, while schema-first parses .graphql files directly.

app.module.tstypescript
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'), // Code-first: generates schema
      sortSchema: true, // Alphabetical ordering for readability
      playground: process.env.NODE_ENV !== 'production', // Disable in prod
      introspection: process.env.NODE_ENV !== 'production',
    }),
  ],
})
export class AppModule {}

The autoSchemaFile option enables code-first mode. Setting it to true generates the schema in memory without writing to disk—useful for serverless deployments where filesystem access may be restricted.

Defining GraphQL Types with Code-First Decorators

Code-first defines GraphQL types using TypeScript classes decorated with @ObjectType(). Each field uses @Field() to specify its GraphQL type and nullability.

user.entity.tstypescript
import { ObjectType, Field, ID, Int } from '@nestjs/graphql';

@ObjectType({ description: 'Application user' }) // Description appears in schema docs
export class User {
  @Field(() => ID) // Maps to GraphQL ID scalar
  id: string;

  @Field()
  email: string;

  @Field({ nullable: true }) // Optional field in GraphQL
  displayName?: string;

  @Field(() => Int, { defaultValue: 0 })
  postCount: number;

  @Field(() => [Post], { nullable: 'itemsAndList' }) // Both list and items can be null
  posts?: Post[];

  // Fields without @Field() are excluded from GraphQL schema
  passwordHash: string;
}

The nullable option accepts three values: true (field is optional), 'items' (list items can be null), and 'itemsAndList' (both the list and items can be null). This granularity matches GraphQL's nullability semantics precisely.

Building Resolvers for Queries and Mutations

Resolvers handle incoming GraphQL operations. NestJS uses @Resolver() to mark a class as a resolver, with method decorators specifying operation types.

users.resolver.tstypescript
import { Resolver, Query, Mutation, Args, ID } from '@nestjs/graphql';
import { User } from './user.entity';
import { UsersService } from './users.service';
import { CreateUserInput } from './dto/create-user.input';

@Resolver(() => User) // Binds resolver to User type for field resolution
export class UsersResolver {
  constructor(private readonly usersService: UsersService) {}

  @Query(() => [User], { name: 'users' }) // Explicit query name
  findAll(): Promise<User[]> {
    return this.usersService.findAll();
  }

  @Query(() => User, { nullable: true })
  user(@Args('id', { type: () => ID }) id: string): Promise<User | null> {
    return this.usersService.findOne(id);
  }

  @Mutation(() => User)
  createUser(@Args('input') input: CreateUserInput): Promise<User> {
    return this.usersService.create(input);
  }
}

The @Resolver(() => User) decorator establishes context for @ResolveField() decorators, enabling field-level resolution for computed or related data.

Input Types and Validation with class-validator

GraphQL input types define mutation payloads. Combining @InputType() with class-validator decorators enables schema-level and runtime validation.

dto/create-user.input.tstypescript
import { InputType, Field } from '@nestjs/graphql';
import { IsEmail, MinLength, IsOptional, Matches } from 'class-validator';

@InputType()
export class CreateUserInput {
  @Field()
  @IsEmail({}, { message: 'Invalid email format' })
  email: string;

  @Field()
  @MinLength(8, { message: 'Password must be at least 8 characters' })
  @Matches(/[A-Z]/, { message: 'Password must contain uppercase letter' })
  password: string;

  @Field({ nullable: true })
  @IsOptional()
  @MinLength(2)
  displayName?: string;
}

Enable validation globally by adding the ValidationPipe in main.ts. GraphQL errors include validation messages in the extensions field, maintaining client compatibility.

Ready to ace your Node.js / NestJS interviews?

Practice with our interactive simulators, flashcards, and technical tests.

Field resolvers handle relationships between types. Without optimization, fetching a list of users with their posts triggers N+1 queries—one for users, then one per user for posts.

users.resolver.tstypescript
import { Resolver, ResolveField, Parent } from '@nestjs/graphql';
import { User } from './user.entity';
import { Post } from '../posts/post.entity';
import { PostsLoader } from '../posts/posts.loader';

@Resolver(() => User)
export class UsersResolver {
  constructor(private readonly postsLoader: PostsLoader) {}

  @ResolveField(() => [Post])
  async posts(@Parent() user: User): Promise<Post[]> {
    // DataLoader batches requests: one query for all user IDs
    return this.postsLoader.batchByUserId.load(user.id);
  }

  @ResolveField(() => Int)
  async postCount(@Parent() user: User): Promise<number> {
    const posts = await this.postsLoader.batchByUserId.load(user.id);
    return posts.length;
  }
}

DataLoader batches and caches requests within a single GraphQL operation. For NestJS, scope the DataLoader to request level using @Injectable({ scope: Scope.REQUEST }).

posts/posts.loader.tstypescript
import { Injectable, Scope } from '@nestjs/common';
import * as DataLoader from 'dataloader';
import { PostsService } from './posts.service';
import { Post } from './post.entity';

@Injectable({ scope: Scope.REQUEST }) // New instance per request
export class PostsLoader {
  constructor(private readonly postsService: PostsService) {}

  public readonly batchByUserId = new DataLoader<string, Post[]>(
    async (userIds: readonly string[]) => {
      // Single query: SELECT * FROM posts WHERE user_id IN (...)
      const posts = await this.postsService.findByUserIds([...userIds]);
      // Map results back to input order
      const postsMap = new Map<string, Post[]>();
      posts.forEach(post => {
        const existing = postsMap.get(post.userId) || [];
        postsMap.set(post.userId, [...existing, post]);
      });
      return userIds.map(id => postsMap.get(id) || []);
    }
  );
}

DataLoader reduces N+1 queries to a single batched query, critical for GraphQL APIs where clients control query depth. For deeper coverage of NestJS architecture patterns, see the NestJS Modules & Dependency Injection interview questions.

Subscriptions for Real-Time Data

GraphQL subscriptions push data to clients over WebSocket connections. NestJS uses the graphql-ws library, which implements the GraphQL over WebSocket protocol.

app.module.ts - Enable subscriptionstypescript
GraphQLModule.forRoot<ApolloDriverConfig>({
  driver: ApolloDriver,
  autoSchemaFile: true,
  subscriptions: {
    'graphql-ws': true, // Modern protocol
    'subscriptions-transport-ws': false, // Deprecated legacy protocol
  },
}),
posts.resolver.tstypescript
import { Resolver, Subscription } from '@nestjs/graphql';
import { PubSub } from 'graphql-subscriptions';
import { Post } from './post.entity';

const pubSub = new PubSub(); // Use Redis PubSub for multi-instance deployments

@Resolver(() => Post)
export class PostsResolver {
  @Subscription(() => Post, {
    filter: (payload, variables) => 
      payload.postCreated.userId === variables.userId, // Client-side filtering
  })
  postCreated() {
    return pubSub.asyncIterableIterator('postCreated');
  }

  @Mutation(() => Post)
  async createPost(@Args('input') input: CreatePostInput): Promise<Post> {
    const post = await this.postsService.create(input);
    pubSub.publish('postCreated', { postCreated: post }); // Trigger subscription
    return post;
  }
}

For production deployments across multiple instances, replace the in-memory PubSub with graphql-redis-subscriptions to broadcast events cluster-wide.

Authentication and Authorization in GraphQL

NestJS Guards work seamlessly with GraphQL resolvers. The execution context differs from REST—use GqlExecutionContext to extract the request.

guards/gql-auth.guard.tstypescript
import { Injectable, ExecutionContext } 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; // Extract request from GraphQL context
  }
}

Apply guards at the resolver or method level. Field-level authorization uses @ResolveField() with conditional logic based on user roles.

users.resolver.tstypescript
@UseGuards(GqlAuthGuard)
@Resolver(() => User)
export class UsersResolver {
  @Query(() => User)
  me(@CurrentUser() user: User): User {
    return user; // Return authenticated user
  }

  @ResolveField(() => String, { nullable: true })
  email(@Parent() user: User, @CurrentUser() currentUser: User): string | null {
    // Only return email if viewing own profile or admin
    if (user.id === currentUser.id || currentUser.role === 'ADMIN') {
      return user.email;
    }
    return null;
  }
}

For complex authorization, consider GraphQL Shield or NestJS's built-in CASL integration. The NestJS Guards and Interceptors article covers these patterns in depth.

Common NestJS GraphQL Interview Questions

Technical interviews for NestJS positions frequently include GraphQL-specific questions. Here are the patterns hiring managers evaluate.

Q: How does NestJS handle the N+1 problem in GraphQL?

DataLoader batches field resolver calls within a single request. When multiple parent objects request the same field, DataLoader collects all keys, executes one batched query, and distributes results. The loader must be request-scoped to prevent cross-request caching issues.

Q: What's the difference between code-first and schema-first in NestJS GraphQL?

Code-first generates the GraphQL schema from TypeScript decorators at runtime, keeping types and schema synchronized automatically. Schema-first parses .graphql SDL files, requiring manual type definitions. Code-first suits TypeScript-native teams; schema-first works better when the schema is the contract between frontend and backend teams.

Q: How would you implement field-level permissions?

Three approaches exist: (1) @ResolveField() with conditional returns based on user context, (2) Custom decorators that check permissions before field resolution, (3) Schema directives like @auth(requires: ADMIN) processed by a directive transformer. The first approach offers the most flexibility; directives provide the cleanest schema documentation.

Q: Explain GraphQL context in NestJS.

The context object passes through all resolvers within a request. NestJS populates it with the HTTP request by default. Custom context is configured in GraphQLModule.forRoot() via the context option—useful for adding DataLoader instances, authenticated user, or database connections.

Q: How do subscriptions scale across multiple server instances?

In-memory PubSub only works for single-instance deployments. Multi-instance architectures require an external broker—Redis PubSub is standard. Each server instance subscribes to Redis channels; when any instance publishes an event, all instances receive it and push to connected WebSocket clients.

For additional NestJS interview preparation, explore the Middleware and Interceptors module covering request lifecycle patterns.

Conclusion

  • NestJS GraphQL supports both code-first and schema-first approaches—code-first simplifies TypeScript projects, schema-first suits SDL-driven workflows
  • DataLoader eliminates N+1 queries by batching field resolver requests within a single operation
  • Request-scoped DataLoader instances prevent cache pollution across concurrent requests
  • GqlExecutionContext bridges NestJS Guards with GraphQL's resolver context
  • Production subscriptions require Redis PubSub for multi-instance event distribution
  • Field-level authorization combines @ResolveField() with user context checks

Start practicing!

Test your knowledge with our interview simulators and technical tests.

Tags

#nestjs
#graphql
#nodejs
#typescript
#api
#tutorial

Share

Related articles