Suggest an editImprove this articleRefine the answer for “How to build a GraphQL API with NestJS?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**GraphQL API with NestJS** is built using `@nestjs/graphql` with Apollo driver - TypeScript decorators define the schema, resolvers handle queries and mutations. ```typescript GraphQLModule.forRoot<ApolloDriverConfig>({ driver: ApolloDriver, autoSchemaFile: 'schema.gql', }) ``` **Key:** use DataLoader in request-scoped providers to avoid N+1 queries on nested fields.Shown above the full answer for quick recall.Answer (EN)Image**GraphQL API with NestJS** is an Apollo-backed setup where TypeScript class decorators define your schema, resolvers handle queries and mutations, and the framework wires everything together automatically. ## Theory ### TL;DR - Install `@nestjs/graphql`, `@nestjs/apollo`, `@apollo/server`, and `graphql` to get started - Code-first: write TypeScript classes with decorators, get a `.gql` schema generated on startup - Schema-first: write the SDL file yourself, NestJS generates TypeScript types from it - Resolvers replace controllers - `@Query`, `@Mutation`, `@ResolveField` replace HTTP verbs - DataLoader is the fix for N+1 queries on nested fields; skip it and nested relations will kill performance ### Code-first vs schema-first With code-first, your TypeScript classes are the schema. NestJS reads the decorators and writes `schema.gql` automatically. With schema-first, you author the `.graphql` SDL file and NestJS generates matching TypeScript types from it. Most teams pick code-first because the TypeScript types and the GraphQL schema stay in sync without extra steps. Schema-first fits when a separate team owns the schema contract, or when you're sharing a schema package across multiple services. ### Setup ```bash npm install @nestjs/graphql @nestjs/apollo @apollo/server graphql ``` ```typescript // app.module.ts import { GraphQLModule } from '@nestjs/graphql'; import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo'; @Module({ imports: [ GraphQLModule.forRoot<ApolloDriverConfig>({ driver: ApolloDriver, autoSchemaFile: 'schema.gql', // scans decorators, writes schema on startup sortSchema: true, playground: true, // disable this in production }), ], }) export class AppModule {} ``` Set `autoSchemaFile: true` if you don't need the file on disk - the schema is generated in memory only. ### Object types and input types Every GraphQL type you return from a query needs `@ObjectType()`. Every field needs `@Field()`. NestJS cannot infer array types or GraphQL scalars like `ID` and `Int` from TypeScript alone, so you pass them explicitly as the first argument. ```typescript import { ObjectType, Field, ID, Int } from '@nestjs/graphql'; @ObjectType() export class User { @Field(() => ID) id: string; @Field() name: string; @Field(() => Int) age: number; @Field(() => [Post], { nullable: true }) posts?: Post[]; } ``` For mutations, use `@InputType()`. You can generate a partial version with `PartialType` - but import it from `@nestjs/graphql`, not `@nestjs/mapped-types`. The mapped-types version skips `@Field()` decorators on the generated fields. ```typescript import { InputType, Field, PartialType } from '@nestjs/graphql'; @InputType() export class CreateUserInput { @Field() name: string; @Field() email: string; @Field(() => Int) age: number; } @InputType() export class UpdateUserInput extends PartialType(CreateUserInput) {} ``` ### Resolvers Resolvers are where business logic connects to GraphQL. `@Resolver(() => User)` binds the class to the `User` type, which matters specifically for `@ResolveField` - without it, field resolvers won't know which type they belong to. ```typescript import { Resolver, Query, Mutation, Args, ResolveField, Parent } from '@nestjs/graphql'; @Resolver(() => User) export class UsersResolver { constructor( private usersService: UsersService, private postsService: PostsService, ) {} @Query(() => [User], { name: 'users' }) findAll() { return this.usersService.findAll(); } @Query(() => User, { name: 'user' }) findOne(@Args('id', { type: () => ID }) id: string) { return this.usersService.findOne(id); } @Mutation(() => User) createUser(@Args('input') input: CreateUserInput) { return this.usersService.create(input); } @Mutation(() => Boolean) deleteUser(@Args('id', { type: () => ID }) id: string) { return this.usersService.delete(id); } @ResolveField(() => [Post]) posts(@Parent() user: User) { return this.postsService.findByUserId(user.id); // runs once per User in the result } } ``` `@ResolveField` runs for every object in the result set. Return 50 users and you make 50 separate calls to `findByUserId`. That is the N+1 problem. ### Authentication NestJS guards work in GraphQL with one required change: you extract the request from the GraphQL execution context, not the HTTP context. ```typescript import { ExecutionContext, Injectable } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { GqlExecutionContext } from '@nestjs/graphql'; @Injectable() export class GqlAuthGuard extends AuthGuard('jwt') { getRequest(context: ExecutionContext) { const ctx = GqlExecutionContext.create(context); return ctx.getContext().req; // reads the HTTP request from inside GraphQL context } } // In resolver @Query(() => User) @UseGuards(GqlAuthGuard) me(@CurrentUser() user: User) { return user; } ``` Without the `getRequest` override, the JWT strategy reads from the HTTP context which in GraphQL returns nothing. The guard passes silently and no token is validated. ### Subscriptions Subscriptions use WebSockets. You enable them in the module config and set up a `PubSub` instance to pass events between mutations and subscribers. ```typescript // Enable in module GraphQLModule.forRoot<ApolloDriverConfig>({ driver: ApolloDriver, autoSchemaFile: 'schema.gql', subscriptions: { 'graphql-ws': true, // modern protocol; 'subscriptions-transport-ws' is legacy }, }) // In resolver @Subscription(() => Message, { filter: (payload, variables) => payload.messageAdded.roomId === variables.roomId, }) messageAdded(@Args('roomId') roomId: string) { return pubSub.asyncIterator('messageAdded'); } @Mutation(() => Message) async sendMessage(@Args('input') input: SendMessageInput) { const message = await this.messagesService.create(input); pubSub.publish('messageAdded', { messageAdded: message }); return message; } ``` For production, replace the in-memory `PubSub` from `graphql-subscriptions` with a Redis-backed one (`graphql-redis-subscriptions`). The in-memory version breaks when you run more than one server instance. ### DataLoader and the N+1 problem DataLoader batches all individual `.load(id)` calls made during a single tick into one function call. Instead of 50 database queries, you get one. ```typescript import DataLoader from 'dataloader'; import { Injectable, Scope } from '@nestjs/common'; @Injectable({ scope: Scope.REQUEST }) // must be request-scoped, not singleton export class PostsLoader { constructor(private postsService: PostsService) {} readonly batchPosts = new DataLoader<string, Post[]>( async (userIds: string[]) => { const posts = await this.postsService.findByUserIds([...userIds]); // return results in the same order as input ids - DataLoader requires this return userIds.map((id) => posts.filter((p) => p.userId === id)); }, ); } // In resolver - replace direct service call with loader @ResolveField(() => [Post]) posts(@Parent() user: User) { return this.postsLoader.batchPosts.load(user.id); } ``` `scope: Scope.REQUEST` is not optional. A singleton DataLoader lives forever and caches results from one request into the next. That means user A can see user B's data. ### Common mistakes **Importing `PartialType` from the wrong package.** ```typescript // Wrong - skips @Field() decorators on generated fields import { PartialType } from '@nestjs/mapped-types'; // Right - preserves @Field() for GraphQL schema generation import { PartialType } from '@nestjs/graphql'; ``` **Not scoping DataLoader to REQUEST.** ```typescript // Wrong - caches data across all requests @Injectable() export class PostsLoader { ... } // Right - resets on every request @Injectable({ scope: Scope.REQUEST }) export class PostsLoader { ... } ``` **Missing `getRequest` override in auth guards.** The guard compiles and runs, but reads from the wrong context. No error is thrown. The token is never validated. **Leaving `playground: true` in production.** The playground exposes your full schema to anyone who opens the URL. Disable it or add auth in front of it. **Using in-memory PubSub for subscriptions beyond one server instance.** Events published on server A never reach clients connected to server B. ### Real-world usage - Products with web and mobile clients that need different data shapes are the strongest case for GraphQL - `@ResolveField` + DataLoader handles deep nesting: User -> Posts -> Comments -> Likes - Subscriptions handle chat, notifications, and live dashboards - Schema-first works well in monorepos where the schema file is shared with frontend code generation tools like GraphQL Code Generator - The `name` option in `@Query(() => User, { name: 'user' })` controls the operation name in the schema - by default NestJS uses the method name ### Follow-up questions **Q:** What is the difference between `@Query` and `@ResolveField` in NestJS? **A:** `@Query` defines a top-level operation on the schema's Query type - it runs once per request. `@ResolveField` defines a field resolver for a specific object type - it runs once per object in the result, which is why N+1 happens there. **Q:** Why does DataLoader require `scope: Scope.REQUEST`? **A:** DataLoader caches results by key for the duration of its lifetime. A singleton DataLoader lives as long as the server, so cached results from one user's request persist into the next user's request. Request scope resets the cache with every incoming request. **Q:** How does auth work differently in GraphQL vs REST in NestJS? **A:** The JWT strategy is the same. The difference is in where the guard reads the request from. REST uses `context.switchToHttp().getRequest()`. In GraphQL you need `GqlExecutionContext.create(context).getContext().req` because the execution context has a different shape inside Apollo. **Q:** When would you choose schema-first over code-first? **A:** Schema-first fits when the GraphQL schema is a shared contract agreed between teams before implementation. If a frontend team and backend team define the schema upfront, schema-first keeps that contract as an explicit file. Code-first fits when one team owns both sides and prefers TypeScript as the single source of truth. **Q:** You return 100 users each with a `posts` field. How many database queries run without DataLoader, and how many with it? **A:** Without DataLoader: 101 queries (1 for users, 100 for posts). With DataLoader: 2 queries (1 for users, 1 batched call with all 100 user IDs). DataLoader collects all the `.load(id)` calls made in a single event loop tick, then fires one batch function. ## Examples ### Basic query and mutation resolver Minimum wiring to get a working GraphQL endpoint with read and write operations. ```typescript // user.type.ts import { ObjectType, Field, ID } from '@nestjs/graphql'; @ObjectType() export class User { @Field(() => ID) id: string; @Field() name: string; @Field() email: string; } // create-user.input.ts import { InputType, Field } from '@nestjs/graphql'; @InputType() export class CreateUserInput { @Field() name: string; @Field() email: string; } // users.resolver.ts @Resolver(() => User) export class UsersResolver { constructor(private usersService: UsersService) {} @Query(() => [User], { name: 'users' }) findAll() { return this.usersService.findAll(); } @Mutation(() => User) createUser(@Args('input') input: CreateUserInput) { return this.usersService.create(input); } } ``` With the app running, open `http://localhost:3000/graphql` and run `query { users { id name email } }`. The schema was generated from decorators on startup. ### Nested field resolution with DataLoader This is the pattern that separates a working GraphQL API from a performant one. ```typescript // posts.loader.ts @Injectable({ scope: Scope.REQUEST }) export class PostsLoader { constructor(private postsService: PostsService) {} readonly batchPosts = new DataLoader<string, Post[]>( async (userIds: string[]) => { const posts = await this.postsService.findByUserIds([...userIds]); return userIds.map((id) => posts.filter((p) => p.userId === id)); }, ); } // users.resolver.ts - extend the existing resolver @ResolveField(() => [Post]) posts(@Parent() user: User) { return this.postsLoader.batchPosts.load(user.id); } ``` `query { users { name posts { title } } }` now triggers exactly two database calls regardless of how many users are returned. I've seen this change take a GraphQL endpoint from 400ms to under 40ms on a real product - it's the single highest-impact optimization in any NestJS GraphQL codebase. ### Protected mutation with JWT guard ```typescript // gql-auth.guard.ts @Injectable() export class GqlAuthGuard extends AuthGuard('jwt') { getRequest(context: ExecutionContext) { const ctx = GqlExecutionContext.create(context); return ctx.getContext().req; } } // users.resolver.ts @Mutation(() => User) @UseGuards(GqlAuthGuard) updateProfile( @CurrentUser() currentUser: User, @Args('input') input: UpdateUserInput, ) { return this.usersService.update(currentUser.id, input); } ``` The client sends `Authorization: Bearer <token>` as an HTTP header. The guard reads it through the GraphQL context bridge, validates the JWT, and `@CurrentUser()` pulls the decoded user off the request object. The mutation only executes if the token is valid.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.