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 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.
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.
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.
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.
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.
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.
Resolving Related Data with @ResolveField and DataLoader
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.
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 }).
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.
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
autoSchemaFile: true,
subscriptions: {
'graphql-ws': true, // Modern protocol
'subscriptions-transport-ws': false, // Deprecated legacy protocol
},
}),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.
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.
@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
GqlExecutionContextbridges 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
Share
Related articles

NestJS + Prisma: The Modern Backend Stack for Node.js
Complete guide to building a modern backend API with NestJS and Prisma. Setup, models, services, transactions and best practices explained.

NestJS: Building a Complete REST API
Complete guide to building a professional REST API with NestJS. Controllers, Services, Modules, validation with class-validator and error handling explained.

Microservices with NestJS in 2026: Architecture, gRPC and Interview Questions
A practical guide to NestJS microservices architecture with gRPC, covering service boundaries, transport layers, streaming patterns, and common interview questions for 2026.