Skip to content
All essays
WebMarch 27, 202618 min

Building GraphQL APIs with NestJS

Learn how to create powerful and efficient GraphQL APIs using NestJS, from setup to advanced features like subscriptions and data loaders

Ü
Ümit Uz
Mobile & Full Stack Developer

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:

bash
npm install @nestjs/graphql graphql @apollo/server graphql-tools
npm install --save-dev @types/graphql

Then, import the GraphQL module in your application:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
// 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:

typescript
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.

Enjoyed this essay? Share it.

Related essays

Next essay
Testing NestJS Applications: From Unit to E2E