GraphQL has revolutionized how we build APIs, giving clients the power to request exactly the data they need. NestJS provides excellent first-class support for GraphQL through the @nestjs/graphql package, making it easy to build type-safe, performant GraphQL APIs. Let's explore how to harness this powerful combination.
Getting Started
NestJS supports two popular GraphQL implementations: Apollo Server and GraphQL Yoga. Both integrate seamlessly with NestJS's dependency injection system and module architecture. We'll focus on Apollo Server, which is the most widely used.
Installation
First, install the required dependencies:
npm install @nestjs/graphql graphql @apollo/server graphql-tools
npm install --save-dev @types/graphqlThen, import the GraphQL module in your application:
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
@Module({
imports: [
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
autoSchemaFile: true,
playground: true,
debug: true,
}),
],
})
export class AppModule {}The autoSchemaFile option tells NestJS to automatically generate the GraphQL schema from your TypeScript classes and decorators.
Code-First Approach
NestJS supports both code-first and schema-first approaches. The code-first approach uses TypeScript decorators to define your GraphQL schema, which is excellent for maintaining type safety:
import { Field, Int, ObjectType, ID } from '@nestjs/graphql';
@ObjectType()
export class User {
@Field((type) => ID)
id: string;
@Field()
username: string;
@Field()
email: string;
@Field((type) => String, { nullable: true })
bio?: string;
@Field((type) => Int)
age: number;
@Field((type) => Date)
createdAt: Date;
@Field((type) => [Post])
posts?: Post[];
}
@ObjectType()
export class Post {
@Field((type) => ID)
id: string;
@Field()
title: string;
@Field((type) => String, { nullable: true })
content?: string;
@Field((type) => User)
author: User;
@Field((type) => Date)
createdAt: Date;
}Decorators like @ObjectType, @Field, and @ID define the GraphQL types. The TypeScript types are automatically inferred from these decorators.
Resolvers
Resolvers handle the logic for fetching data for your GraphQL operations:
import { Resolver, Query, Mutation, Args, Int, Parent, ResolveField } from '@nestjs/graphql';
import { User, Post } from './models';
import { UsersService } from './users.service';
@Resolver((of) => User)
export class UsersResolver {
constructor(private usersService: UsersService) {}
@Query((returns) => [User])
async users(): Promise<User[]> {
return this.usersService.findAll();
}
@Query((returns) => User)
async user(@Args('id', { type: () => String }) id: string): Promise<User> {
return this.usersService.findOne(id);
}
@Mutation((returns) => User)
async createUser(
@Args('username') username: string,
@Args('email') email: string,
@Args('password') password: string,
): Promise<User> {
return this.usersService.create({ username, email, password });
}
@ResolveField('posts', (returns) => [Post])
async getPosts(@Parent() user: User) {
return this.usersService.getPosts(user.id);
}
}The @Resolver decorator defines the resolver class, while @Query and @Mutation decorators define the operations. @ResolveField handles nested field resolution.
Input Types and Arguments
For complex mutations and queries, you should use input types:
import { InputType, Field } from '@nestjs/graphql';
@InputType()
export class CreateUserInput {
@Field()
username: string;
@Field()
email: string;
@Field()
password: string;
@Field((type) => String, { nullable: true })
bio?: string;
}
@InputType()
export class UpdateUserInput {
@Field((type) => String, { nullable: true })
username?: string;
@Field((type) => String, { nullable: true })
email?: string;
@Field((type) => String, { nullable: true })
bio?: string;
}
// Use in resolver
@Mutation((returns) => User)
async updateUser(
@Args('id') id: string,
@Args('input') input: UpdateUserInput,
): Promise<User> {
return this.usersService.update(id, input);
}Input types define the shape of data sent to mutations and queries.
Validation
Combine GraphQL with class-validator for automatic validation:
import { InputType, Field } from '@nestjs/graphql';
import { IsEmail, IsString, MinLength } from 'class-validator';
@InputType()
export class CreateUserInput {
@Field()
@IsString()
@MinLength(3)
username: string;
@Field()
@IsEmail()
email: string;
@Field()
@IsString()
@MinLength(6)
password: string;
}
// Enable validation in your resolver
@Resolver((of) => User)
export class UsersResolver {
constructor(private usersService: UsersService) {}
@Mutation((returns) => User)
async createUser(@Args('input') input: CreateUserInput): Promise<User> {
// Validation happens automatically
return this.usersService.create(input);
}
}Validation errors are automatically returned to the client in a GraphQL-compliant format.
Authentication and Authorization
Implement authentication using guards and custom decorators:
import { CanActivate, ExecutionContext, Injectable, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { GqlExecutionContext } from '@nestjs/graphql';
import { Resolver, Query, Mutation, Args, Context } from '@nestjs/graphql';
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
// Custom decorator to get user from context
export const CurrentUser = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const context = GqlExecutionContext.create(ctx);
return context.getContext().req.user;
},
);
// GraphQL Auth Guard
@Injectable()
export class GqlAuthGuard extends AuthGuard('jwt') {
getRequest(context: ExecutionContext) {
const ctx = GqlExecutionContext.create(context);
return ctx.getContext().req;
}
}
// Use in resolver
@Resolver((of) => User)
export class UsersResolver {
@Query((returns) => User)
@UseGuards(GqlAuthGuard)
async me(@CurrentUser() user: User): Promise<User> {
return user;
}
@Mutation((returns) => User)
@UseGuards(GqlAuthGuard)
async updateProfile(
@CurrentUser() user: User,
@Args('input') input: UpdateUserInput,
): Promise<User> {
return this.usersService.update(user.id, input);
}
}This pattern protects your GraphQL endpoints while providing access to the authenticated user.
Data Loaders and N+1 Problem
The N+1 query problem is common in GraphQL. Use data loaders to batch requests:
import { DataLoaderFactory } from './dataloader.factory';
import { UsersService } from './users.service';
@Resolver((of) => Post)
export class PostsResolver {
constructor(
private usersService: UsersService,
private dataLoaderFactory: DataLoaderFactory,
) {}
@ResolveField('author', (returns) => User)
async getAuthor(@Parent() post: Post) {
const loader = this.dataLoaderFactory.getOrCreateLoader(
'user',
this.usersService.batchFindUsers,
);
return loader.load(post.authorId);
}
}
// Data loader factory
import { DataLoader } from 'dataloader';
import { UsersService } from './users.service';
@Injectable()
export class DataLoaderFactory {
private loaders: Map<string, any> = new Map();
getOrCreateLoader<T, K>(name: string, batchLoadFn: (ids: K[]) => Promise<T[]>) {
if (!this.loaders.has(name)) {
this.loaders.set(name, new DataLoader(batchLoadFn));
}
return this.loaders.get(name);
}
}Data loaders batch multiple requests into a single query, dramatically improving performance.
Subscriptions
GraphQL subscriptions enable real-time updates using WebSockets:
import { Resolver, Query, Subscription, Mutation, Args } from '@nestjs/graphql';
import { PubSub } from 'graphql-subscriptions';
const PUB_SUB = 'PUB_SUB';
@Resolver((of) => Post)
export class PostsResolver {
constructor(
private postsService: PostsService,
@Inject(PUB_SUB) private pubSub: PubSub,
) {}
@Query((returns) => [Post])
async posts() {
return this.postsService.findAll();
}
@Mutation((returns) => Post)
async createPost(
@Args('title') title: string,
@Args('content') content: string,
) {
const post = await this.postsService.create({ title, content });
this.pubSub.publish('postCreated', { postCreated: post });
return post;
}
@Subscription((returns) => Post, {
filter: (payload, variables) => {
return payload.postCreated.authorId === variables.authorId;
},
})
postCreated(@Args('authorId') authorId: string) {
return this.pubSub.asyncIterator('postCreated');
}
}
// Configure subscriptions in module
@Module({
providers: [
{
provide: PUB_SUB,
useValue: new PubSub(),
},
],
})
export class PostsModule {}Subscriptions allow clients to receive real-time updates when data changes.
Error Handling
Implement proper error handling for GraphQL:
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { GqlExceptionFilter, GqlExecutionContext } from '@nestjs/graphql';
@Catch(HttpException)
export class GraphQLExceptionFilter implements GqlExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const context = GqlExecutionContext.create(host);
const response = exception.getResponse();
const status = exception.getStatus();
return {
statusCode: status,
message: response['message'] || exception.message,
timestamp: new Date().toISOString(),
path: context.getContext().req.body,
};
}
}
// Use in resolver
@Resolver((of) => User)
export class UsersResolver {
@Mutation((returns) => User)
async createUser(@Args('input') input: CreateUserInput): Promise<User> {
try {
return await this.usersService.create(input);
} catch (error) {
if (error.code === 'DUPLICATE_USERNAME') {
throw new ConflictException('Username already exists');
}
throw error;
}
}
}Custom exception filters provide fine-grained control over error responses.
Testing GraphQL Resolvers
Test your GraphQL resolvers using NestJS testing utilities:
import { Test } from '@nestjs/testing';
import { UsersResolver } from './users.resolver';
import { UsersService } from './users.service';
import { AppModule } from './app.module';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver } from '@nestjs/apollo';
describe('UsersResolver', () => {
let resolver: UsersResolver;
let usersService: UsersService;
beforeEach(async () => {
const module = await Test.createTestingModule({
imports: [AppModule],
providers: [UsersResolver],
})
.overrideProvider(UsersService)
.useValue({
findAll: jest.fn(() => []),
findOne: jest.fn(() => ({})),
create: jest.fn(() => ({})),
})
.compile();
resolver = module.get<UsersResolver>(UsersResolver);
usersService = module.get<UsersService>(UsersService);
});
it('should return an array of users', async () => {
const result = await resolver.users();
expect(result).toEqual([]);
});
});Testing ensures your resolvers work correctly and handle edge cases.
Performance Optimization
Optimize your GraphQL API performance:
// Enable query complexity analysis
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
autoSchemaFile: true,
complexity: 10,
validationRules: [
createComplexityLimitRule(1000),
],
});
// Use persisted queries
import { usePersistedQueries } from '@apollo/query-planning';
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
plugins: [
usePersistedQueries(),
],
});
// Implement response caching
@Query((returns) => User)
@CacheKey('user')
async user(@Args('id') id: string): Promise<User> {
return this.usersService.findOne(id);
}These techniques help your GraphQL API handle high loads efficiently.
Federation
For microservices, use Apollo Federation:
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloFederationDriver, ApolloFederationDriverConfig } from '@nestjs/apollo';
@Module({
imports: [
GraphQLModule.forRoot<ApolloFederationDriverConfig>({
driver: ApolloFederationDriver,
autoSchemaFile: {
federation: true,
},
}),
],
})
export class AppModule {}
// Define federated entity
@ObjectType()
@KeyFields('id')
export class User {
@Field((type) => ID)
id: string;
@Field()
username: string;
@Field()
email: string;
}Federation allows you to compose multiple GraphQL services into a unified graph.
Conclusion
NestJS and GraphQL together provide a powerful combination for building flexible, type-safe APIs. The code-first approach keeps your TypeScript types and GraphQL schema in sync, while the modular architecture keeps your code organized. By leveraging resolvers, subscriptions, data loaders, and the rich ecosystem of tools, you can build sophisticated APIs that serve clients efficiently.
As you continue building with GraphQL in NestJS, explore advanced topics like custom directives, middleware, and the various plugins available for Apollo Server. The official documentation and community resources provide excellent guidance for taking your GraphQL APIs to the next level.