GraphQL with NestJS

Integrating GraphQL into a NestJS application using Apollo Server or other GraphQL libraries.


Implementing Resolvers in NestJS with GraphQL

This document explains how to implement resolvers in NestJS for GraphQL APIs, covering queries, mutations, subscriptions, data source access, business logic handling, context, and arguments.

What are Resolvers?

In GraphQL, resolvers are functions that are responsible for fetching the data for a specific field in a GraphQL type. They essentially "resolve" the data requested in a GraphQL query, mutation, or subscription.

Implementing Resolvers in NestJS

NestJS simplifies GraphQL resolver implementation using decorators and a modular structure. Here's a breakdown:

1. Setting up GraphQL Module

First, ensure you've installed and configured the @nestjs/graphql and @nestjs/apollo (or @nestjs/platform-express for code-first approach without SDL) modules in your NestJS application.

 npm install @nestjs/graphql @nestjs/apollo graphql
    # or
    npm install @nestjs/graphql @nestjs/platform-express graphql 

Then, configure the GraphQL module in your AppModule:

 // app.module.ts
    import { Module } from '@nestjs/common';
    import { GraphQLModule } from '@nestjs/graphql';
    import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
    import { AppController } from './app.controller';
    import { AppService } from './app.service';

    @Module({
      imports: [
        GraphQLModule.forRoot({
          driver: ApolloDriver,
          autoSchemaFile: 'schema.gql', // or in-memory: true
          sortSchema: true,
        }),
      ],
      controllers: [AppController],
      providers: [AppService],
    })
    export class AppModule {} 

This sets up the GraphQL module using Apollo Server. The autoSchemaFile option generates a GraphQL schema file based on your resolvers and types.

2. Creating a Resolver

Create a resolver class using NestJS's dependency injection system. Decorate the class with @Resolver() and define your resolver functions within the class. This example assumes a `User` type is defined in your schema (or with decorators using code-first approach).

 // user.resolver.ts
    import { Resolver, Query, Mutation, Args } from '@nestjs/graphql';
    import { UserService } from './user.service';
    import { User } from './user.model'; // Or use @ObjectType() decorator in code-first

    @Resolver(() => User)
    export class UserResolver {
      constructor(private readonly userService: UserService) {}

      @Query(() => [User], { name: 'users' })
      async getUsers(): Promise<User[]> {
        return this.userService.findAll();
      }

      @Query(() => User, { name: 'user' })
      async getUser(@Args('id', { type: () => Int }) id: number): Promise<User> {
        return this.userService.findOne(id);
      }

      @Mutation(() => User)
      async createUser(@Args('name') name: string, @Args('email') email: string): Promise<User> {
        return this.userService.create(name, email);
      }

      @Mutation(() => User)
      async updateUser(@Args('id', { type: () => Int }) id: number, @Args('name') name: string, @Args('email') email: string): Promise<User> {
        return this.userService.update(id, name, email);
      }

      @Mutation(() => Boolean)
      async deleteUser(@Args('id', { type: () => Int }) id: number): Promise<boolean> {
          await this.userService.remove(id);
          return true;
      }
    } 

3. Resolvers for Queries, Mutations, and Subscriptions

  • Queries: Use the @Query() decorator to define resolvers for GraphQL queries. The return type annotation (e.g., `(): Promise<User[]>`) defines the return type of the query. The options object, e.g., `{ name: 'users' }`, renames the query in the schema.
  • Mutations: Use the @Mutation() decorator to define resolvers for GraphQL mutations. Similar to queries, the return type and name can be specified.
  • Subscriptions: Use the @Subscription() decorator to define resolvers for GraphQL subscriptions. You'll need to use a PubSub mechanism (like graphql-subscriptions).
     // Example (simplified):
             import { Subscription } from '@nestjs/graphql';
             import { PubSub } from 'graphql-subscriptions';
    
             const pubSub = new PubSub();
    
             @Subscription(() => User)
             userCreated() {
                 return pubSub.asyncIterator('userCreated');
             }
    
             @Mutation(() => User)
             async createUser(@Args('name') name: string, @Args('email') email: string): Promise<User> {
                 const user = await this.userService.create(name, email);
                 pubSub.publish('userCreated', { userCreated: user });
                 return user;
             } 

4. Accessing Data Sources and Handling Business Logic

Resolvers often need to interact with data sources (databases, APIs, etc.) and apply business logic. This is typically done by injecting services (e.g., UserService in the example above) into the resolver and calling methods on those services.

Data Sources: The UserService would then handle the data access logic, e.g., using TypeORM, Mongoose, or a REST API client to interact with a database or external API.

Business Logic: The service layer is also the ideal place to implement any necessary business logic, such as validation, authorization, or data transformations.

 // user.service.ts
    import { Injectable } from '@nestjs/common';
    import { User } from './user.model';
    import { InjectRepository } from '@nestjs/typeorm';
    import { Repository } from 'typeorm';

    @Injectable()
    export class UserService {
      constructor(
        @InjectRepository(User) // Example using TypeORM
        private usersRepository: Repository<User>,
      ) {}

      async findAll(): Promise<User[]> {
        return this.usersRepository.find();
      }

      async findOne(id: number): Promise<User> {
        return this.usersRepository.findOneBy({ id });
      }

      async create(name: string, email: string): Promise<User> {
        const user = this.usersRepository.create({ name, email });
        return this.usersRepository.save(user);
      }

        async update(id: number, name: string, email: string): Promise<User> {
            const user = await this.usersRepository.findOneBy({ id });
            if (!user) {
                throw new Error(`User with ID ${id} not found`); // Or a custom exception
            }
            user.name = name;
            user.email = email;
            return this.usersRepository.save(user);
        }

        async remove(id: number): Promise<void> {
            await this.usersRepository.delete(id);
        }
    } 

5. Understanding Context and Arguments

Resolvers have access to the GraphQL context and arguments:

  • Context: The context is an object that's passed to every resolver in a particular execution. It typically contains information like the current user, authentication tokens, data loaders, or other request-specific data. Access the context using the @Context() decorator.
  • Arguments: Arguments are the values passed to the GraphQL query, mutation, or subscription. Access arguments using the @Args() decorator, as shown in the UserResolver example. You can also use the @Parent() decorator to access the result of the parent resolver (relevant for field resolvers).
 // Example using Context and Parent

    import { Resolver, Query, Args, Context, Parent, ResolveField } from '@nestjs/graphql';
    import { UserService } from './user.service';
    import { User } from './user.model';
    import { PostService } from './post.service';
    import { Post } from './post.model';

    @Resolver(() => User)
    export class UserResolver {
      constructor(
        private readonly userService: UserService,
        private readonly postService: PostService,
      ) {}

      //... other resolvers ...

      @ResolveField('posts', () => [Post]) //  field resolver for User.posts
      async getPosts(@Parent() user: User): Promise<Post[]> {
          return this.postService.findByUserId(user.id);
      }
    } 

Authentication and Authorization: Use the context to handle authentication and authorization. For example, you might inject a Request object into the context and use it to extract an authentication token from the headers. You can use guards in nestjs to ensure only authenticated users can access resolvers.

Key Considerations

  • Error Handling: Implement proper error handling within your resolvers to provide informative error messages to the client. Use NestJS's exception filters for centralized error handling.
  • Performance: Optimize your resolvers for performance. Consider using data loaders to avoid the N+1 problem, especially when resolving relationships between entities.
  • Modularity: Structure your resolvers and services in a modular way for better maintainability and testability.
  • Testing: Write unit tests for your resolvers and services to ensure they function correctly.