Nest js with graphql setup using nestjs/graphql 🍾🍾

Building graphql apis using nestjs graphql 🍾🍾

GraphQL is a powerful query language for APIs and a runtime for fulfilling those queries with your existing data. It's an elegant approach that solves many problems typically found with REST APIs. For background, we suggest reading this comparison between GraphQL and REST. GraphQL combined with TypeScript helps you develop better type safety with your GraphQL queries, giving you end-to-end typing.

In this chapter, we assume a basic understanding of GraphQL, and focus on how to work with the built-in @nestjs/graphql module. The GraphQLModule can be configured to use Apollo server (with the @nestjs/apollo driver) and Mercurius (with the @nestjs/mercurius). We provide official integrations for these proven GraphQL packages to provide a simple way to use GraphQL with Nest. You can also build your own dedicated driver (read more on that here).

Installation

Start by installing the required packages:

# For Express and Apollo (default)
$ npm i @nestjs/graphql @nestjs/apollo graphql apollo-server-express

# For Fastify and Apollo
# npm i @nestjs/graphql @nestjs/apollo graphql apollo-server-fastify

# For Fastify and Mercurius
# npm i @nestjs/graphql @nestjs/mercurius graphql mercurius

@nestjs/graphql@>=9 and @nestjs/apollo^10 packages are compatible with Apollo v3 (check out Apollo Server 3 migration guide for more details), while @nestjs/graphql@^8 only supports Apollo v2 (e.g., apollo-server-express@2.x.x package).

Overview

Nest offers two ways of building GraphQL applications, the code first and the schema first methods. You should choose the one that works best for you. Most of the chapters in this GraphQL section are divided into two main parts: one you should follow if you adopt code first, and the other to be used if you adopt schema first.

  • schema first approach
  • code first approach

In the code first approach, you use decorators and TypeScript classes to generate the corresponding GraphQL schema. This approach is useful if you prefer to work exclusively with TypeScript and avoid context switching between language syntaxes.

In the schema first approach, the source of truth is GraphQL SDL (Schema Definition Language) files. SDL is a language-agnostic way to share schema files between different platforms. Nest automatically generates your TypeScript definitions (using either classes or interfaces) based on the GraphQL schemas to reduce the need to write redundant boilerplate code.

lets start with simple example

import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
    }),
  ],
})
export class AppModule {}

Code first approach

In the code first approach, you use decorators and TypeScript classes to generate the corresponding GraphQL schema.

To use the code first approach, start by adding the autoSchemaFile property to the options object:

GraphQLModule.forRoot <
  ApolloDriverConfig >
  {
    driver: ApolloDriver,
    autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
  };

Schema first approach

To use the schema first approach, start by adding a typePaths property to the options object. The typePaths property indicates where the GraphQLModule should look for GraphQL SDL schema definition files you'll be writing. These files will be combined in memory; this allows you to split your schemas into several files and locate them near their resolvers.

GraphQLModule.forRoot <
  ApolloDriverConfig >
  {
    driver: ApolloDriver,
    typePaths: ['./**/*.graphql'],
  };

Lets Build something to understand the end to end flow Github: https://github.com/tkssharma/nodejs-graphql-world/tree/master/Graphql%20using%20Apollo/nestjs-graphql-schema-first-%2301

We are going to build Pokemon Graphql service

we will use @nestjs/typeorm @nestjs/graphql @nestjs/config and rest all nestjs core modules

    "@nestjs/apollo": "^10.0.8",
    "@nestjs/common": "^8.0.0",
    "@nestjs/config": "^2.0.0",
    "@nestjs/core": "^8.0.0",
    "@nestjs/graphql": "^10.0.8",
    "@nestjs/platform-express": "^8.0.0",
    "@nestjs/typeorm": "^8.0.3",
    "apollo-server-express": "^3.6.7",
    "fbjs-scripts": "^3.0.1",
    "graphql": "^16.3.0",
    "graphql-tools": "^8.2.3",
    "joi": "^17.6.0",
    "pg": "^8.7.3",
    "reflect-metadata": "^0.1.13",
    "rimraf": "^3.0.2",
    "rxjs": "^7.2.0",
    "ts-morph": "^14.0.0",
    "type-graphql": "^1.1.1",
    "typeorm": "^0.3.4"

Above we have nestjs core dependancies, nestjs typeorm, and nestjs graphql packages I have already craeted github Repo for this example, Here i am just explaining the building blocks for building this whole setup Our main module

import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config'
import * as Joi from 'joi';
import { TypeOrmModule, TypeOrmModuleAsyncOptions } from '@nestjs/typeorm';
import { GraphQLModule } from '@nestjs/graphql';
import { join } from 'path';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { PokemonModule } from './pokemon/pokemon.module';
import { LeagueModule } from './league/league.module';


@Module({
  imports: [
    ConfigModule.forRoot({
      validationSchema: Joi.object({
        NODE_ENV: Joi.string()
          .valid('development', 'production', 'test', 'local')
          .default('development'),
        PORT: Joi.number().default(3000),
        DATABASE_URL: Joi.string().required()
      })
    }),
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (configService: ConfigService) => {
        return {
          name: 'default',
          type: 'postgres',
          logging: true,
          url: configService.get('DATABASE_URL'),
          entities: [__dirname + '/**/**.entity{.ts,.js}'],
          synchronize: true
        } as TypeOrmModuleAsyncOptions;
      }
    }),
    GraphQLModule.forRoot<ApolloDriverConfig>({
      playground: true,
      driver: ApolloDriver,
      typePaths: ['./**/*.graphql'],
      context: ({ req }) => ({ headers: req.headers }),
      debug: true,
      definitions: {
        path: join(process.cwd(), 'src/graphql.schema.ts'),
        outputAs: 'class',
      },
    }),
    PokemonModule, LeagueModule
  ],
  controllers: [],
  providers: [],
})
export class DomainModule { }

TypeORM and nestjs config module we are already aware, here we can talk about graphqlModule using ApolloDriver and we are passing typePaths as this is schema first approach

GraphQLModule.forRoot <
  ApolloDriverConfig >
  {
    playground: true,
    driver: ApolloDriver,
    typePaths: ['./**/*.graphql'],
    context: ({ req }) => ({ headers: req.headers }),
    debug: true,
    definitions: {
      path: join(process.cwd(), 'src/graphql.schema.ts'),
      outputAs: 'class',
    },
  };

We have two module in this application Pokemon and pokemon League both module will have same set of files

  • nestjs DTO file
  • schema file .graphql schema file
  • graphql resolver file
  • nestjs module
  • nestjs service

Lets check one by one, Pokemon module will allow us to create and manage a new pokemon here is the .graphql schema for that module

type Pokemon {
  id: String!
  name: String!
  type: String!
  league: League
}
type Query {
  pokemons: [Pokemon!]
  pokemon(id: ID): Pokemon!
}
type Deleted {
  delete: Boolean!
}
type Mutation {
  create(name: String!, type: String!): Pokemon
  update(id: ID!, name: String!, type: String!): Pokemon
  delete(id: ID!): Deleted
  assign(id: ID!, leagueId: ID!): Pokemon
}

Now we can have resolver to hanale all these queries and mutations

import { Resolver, Query, Args, Mutation } from '@nestjs/graphql';
import { PokemonEntity } from '../entity/pokemon.entity';
import { PokemonService } from './pokemon.service';

@Resolver(of => PokemonEntity)
export class PokemonResolver {
  constructor(private pokemonService: PokemonService) {
  }

  @Query()
  async pokemons() {
    return await this.pokemonService.getPokemons();
  }

  @Mutation()
  async create(@Args('name') name, @Args('type') type) {
    return this.pokemonService.createPokemon({ name, type });
  }

  @Mutation()
  async update(@Args('id') id, @Args('name') name, @Args('type') type) {
    return this.pokemonService.update(id, { name, type });
  }

  @Mutation()
  async assign(@Args('id') id, @Args('leagueId') leagueId) {
    return this.pokemonService.assignLeague(id, leagueId);
  }

  @Mutation()
  async delete(@Args('id') id) {
    await this.pokemonService.delete(id);
    return { delete: true };
  }

  @Query()
  async pokemon(@Args('id') id: string) {
    return await this.pokemonService.show(id);
  }
}

Resolver will have DI with service which will be using TypeORM repository to access database and return data based on typeorm query we are processing This is how our service looks like

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { LeagueEntity } from '../entity/league.entity';
import { PokemonEntity } from '../entity/pokemon.entity';
import { CreatePokemonDto } from './pokemon.dto';

@Injectable()
export class PokemonService {
  constructor(@InjectRepository(PokemonEntity) private readonly pokemonRepository: Repository<PokemonEntity>,
    @InjectRepository(LeagueEntity) private readonly leagueRepository: Repository<LeagueEntity>
  ) {
  }

  async createPokemon(data: CreatePokemonDto): Promise<PokemonEntity> {
    let pokemon = new PokemonEntity();
    pokemon.name = data.name;
    pokemon.type = data.type;
    await pokemon.save();
    return pokemon;
  }

  async delete(id): Promise<PokemonEntity> {
    const pokemon = await this.pokemonRepository.findOne({ where: { id } });
    await this.pokemonRepository.delete(id);
    return pokemon;
  }

  async update(id, data: CreatePokemonDto): Promise<PokemonEntity> {
    const pokemon = await this.pokemonRepository.findOne({ where: { id } });
    pokemon.name = data.name;
    pokemon.type = data.type;
    await pokemon.save();
    return pokemon;
  }
  // assign league to the pokemon
  async assignLeague(id: string, leagueId: string) {
    const pokemon = await this.pokemonRepository.findOne({ where: { id } });
    const league = await this.leagueRepository.findOne({ where: { id: leagueId } });
    pokemon.league = league;
    await pokemon.save();
    return pokemon;
  }

  async show(id: string) {
    return await this.pokemonRepository.findOne({ where: { id } });
  }

  async getPokemons() {
    return await this.pokemonRepository.find({});
  }
}

Similarly we can have another module pokemon league module Graphql schema for this module

type League {
  id: ID!
  name: String!
  pokemons: [Pokemon!]
}

type Query {
  leagues: [League!]
  league(id: ID): League!
}

type Mutation {
  createLeague(name: String!): League
  updateLeague(id: ID!, name: String!): League
  deleteLeague(id: ID): Deleted
}

Once everything is ready we should be able to see application running, we just need to make sure we have typeorm configuration and postgres container running to provide local database. Once everything is done we should be able to see nestjs application running on defined PORT.

Conclusion

We have these two different approach and both works as expected, code first make us to write more code using typescript annotations using @ObjectType, @Field, @InputType, Where is schema first approach we create schema and pass that schema definition to graphqlModule and based on that we define resolvers and services

References

Comments