Working with GraphQL in Nest JS

Working with GraphQL in Nest JS

Graph Query Language or GraphQL is an API query language.

It allows us to fetch precise data per request instead of fetching unneeded data and delivering it to us. GraphQL is an intuitive method of fetching data.

An example scenario is when you are asked what your age is. The person asking only expects your age e.g. 44 years old. You don't begin by mentioning your birth date, where you were born, or even your parents.

The precision in data fetching is the power of GraphQL.

Let us go over a few common terms you will come across when working with GraphQL.

  • Schema: defines the structure of the API. GraphQL has its type system for this and the language used to write the schema is called Schema Definition Language(SDL).

  • Types: are the entities we would like to have in our application. For example, in a notes app, the note will be a type. There are different types namely scalar, enum, union, object, and interface types.

  • Fields: define the data of the types.

type and field example type Note { title: String! body: String! }

In the example above, Post is the type, and title is the field.

Ways to build GraphQL APIs in Nest JS

Schema-first is a method where you manually define the GraphQL schema using SDL, then add the typePaths property to the GraphQL Module options to tell it where to look for the schema.

Code-first is a method where the GraphQL schema is generated from Typescript classes and decorators, you do not write the GraphQL schema yourself.

Code-First method

For this tutorial, we shall use the code-first approach where we shall add autoSchemaFile to our GraphQL module options.

Installing dependencies

Install the following dependencies that we shall use in this project.

npm install @nestjs/graphql graphql @nestjs/apollo @apollo/server typeorm @nestjs/typeorm pg @nestjs/config

Configure TypeORM

We shall use Postgres as our database and TypeORM as our ORM of choice.

For the detailed method to configure Typeorm check out this article and then we can proceed to the next section.

Configure GraphQL in the application module

Since we are using the code-first method, we shall configure the GraphQLModule with the following options:

  • Driver: this is the driver of our GraphQL server.

  • autoSchemaFile: this property's value indicates where the schema will be auto-generated.

  • sortSchema: this property will sort the items in the schema according to the way they would be in a dictionary because when the types are generated as they are in the module files.

COPY

COPY

// in the imports array
GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
      autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
      sortSchema: true,
    }),

Creating the CRUD feature

The beauty of Nest JS is that there is a simple way of building the GraphQL feature depending on the method you prefer whether code-first or schema-first.

Generating the feature using resource

We shall be creating all the CRUD features and as such we will make use of the resource generator.resource generator is a feature that auto-generates boilerplate for resources such as controllers, modules, and services. It helps save time and reduce the repetition of tasks.

Run the command below

nest generate resource note

You will then see a prompt like this asking which transport layer you want.

? What transport layer do you use?
REST API
❯ GraphQL (code first)
GraphQL (schema first)
Microservice (non-HTTP)
WebSockets

GraphQL prompt

Select the GraphQL (code-first) option.

Entity

The entity is where we define our database schema. The Nest JS docs refer to this as the model, but for simplicity let us can them the entity. The definition of our entity shall differ slightly from how it is defined in REST. We shall make use of the @Field() and @ObjectType() decorators.

COPY

COPY

import { ObjectType, Field, Int } from '@nestjs/graphql';
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';

@ObjectType()
@Entity()
export class Note {

  @Field(() => Int)
  @PrimaryGeneratedColumn()
  id: number;

  @Field()
  @Column('text', { nullable: false })
  title: string;

  @Field()
  @Column('text', { nullable: false })
  body: string;

}

Our schema for a note entity shall have two columns the title and body.

The ID shall have the @PrimaryGeneratedColumn() so that it auto-generates IDs for the data we key in and the @Field(() => Int) decorator with the type Int to ensure the type allowed is an integer.

We use the @Column('text', { nullable: false }) on the body and title columns to validate that the input should not be null values.

Data Transfer Object (DTO)

We shall have data transfer object files for both the creation and updation of our files. This will help perform data transformations and validation before storage in the database.

COPY

COPY

import { InputType, Field } from '@nestjs/graphql';

@InputType()
export class CreateNoteInput {
  @Field()
  title: string;

  @Field()
  body: string;
}

COPY

COPY

import { CreateNoteInput } from './create-note.input';
import { InputType, Field, PartialType } from '@nestjs/graphql';

@InputType()
export class UpdateNoteInput extends PartialType(CreateNoteInput) {
  @Field()
  title: string;

  @Field()
  body: string;
}

Unlike the REST format of DTOs here we use the @Column() decorator here we use the @Field() decorator for the database columns.

Service

The service is where we define our business logic functions. This helps keep the resolver modular.

COPY

COPY

// note.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Note } from './entities/note.entity';
import { CreateNoteInput } from './dto/create-note.input';
import { UpdateNoteInput } from './dto/update-note.input';

@Injectable()
export class NoteService {
  constructor(
    @InjectRepository(Note)
    private noteRepository: Repository<Note>,
  ) {}

  findAll(): Promise<Note[]> {
    return this.noteRepository.find();
  }

  findOne(id: number): Promise<Note> {
    return this.noteRepository.findOne({ where: { id } });
  }

  async create(createNoteInput: CreateNoteInput): Promise<Note> {
    const note = this.noteRepository.create(createNoteInput);
    return this.noteRepository.save(note);
  }

  async update(id: number, updateNoteInput: UpdateNoteInput) {
    const note = await this.findOne(id);
    this.noteRepository.merge(note, updateNoteInput);
    return this.noteRepository.save(note);
  }

  async remove(id: number): Promise<void> {
    await this.noteRepository.delete(id);
  }
}
  • The create function is for creating a new note based on the data in the createNoteInput DTO and then it saves the note using the create and save methods.

  • The update function is for updating a note based on the data provided in the updateNoteInput DTO. It retrieves the note using its id and then merges the changes and then updates it in the database.

Resolver

A resolver is a file that provides instructions for turning GraphQL operations into data. They use the schema to return the data as specified.

This is the equivalent of controllers in REST.

The GraphQL operations that can be performed on data include:

  • Mutation: is used to make writes or updates to data in a GraphQL server. Operations like create, update, and delete are done using mutations.

  • Query: is used to fetch data from a GraphQL server. It is a read operation that requests data without making any write operations in the database.

  • Subscriptions: allow for real-time server updates. They enable the server to push data to clients when an event occurs. A use case for subscriptions is notifications.

COPY

COPY

import { Resolver, Query, Mutation, Args, Int } from '@nestjs/graphql';
import { NoteService } from './note.service';
import { Note } from './entities/note.entity';
import { CreateNoteInput } from './dto/create-note.input';
import { UpdateNoteInput } from './dto/update-note.input';

@Resolver(() => Note)
export class NoteResolver {
  constructor(private readonly noteService: NoteService) {}

  @Mutation(() => Note)
  async createNote(@Args('createNoteInput') createNoteInput: CreateNoteInput) {
    return this.noteService.create(createNoteInput);
  }

  @Query(() => [Note], { name: 'notes' })
  findAll() {
    return this.noteService.findAll();
  }

  @Query(() => Note, { name: 'note' })
  findOne(@Args('id', { type: () => Int }) id: number) {
    return this.noteService.findOne(id);
  }

  @Mutation(() => Note)
  updateNote(@Args('updateNoteInput') updateNoteInput: UpdateNoteInput) {
    return this.noteService.update(updateNoteInput.id, updateNoteInput);
  }

  @Mutation(() => Note)
  removeNote(@Args('id', { type: () => Int }) id: number) {
    return this.noteService.remove(id);
  }
}
  • @Resolver(() => Note): this decorator shows NoteResolver class is a GraphQL resolver for the 'Note' entity telling NestJS and the GraphQL framework that this class handles GraphQL queries and mutations related to the Note type.

  • createNote mutation: The @Args('createNoteInput') decorator specifies that the createNote method expects an argument named createNoteInput, which should be of type CreateNoteInput.
    Inside the method, you call this.noteService.create(createNoteInput) function to create a new note and return it.

  • findAll query: This decorator marks the findAll method as a GraphQL query. Queries are used for retrieving data from the server.
    The { name: 'notes' } option specifies the name of the query, which will be used in the GraphQL schema.
    Inside the method, you call this.noteService.findAll() to retrieve a list of all notes and return them.

  • findOne query: This decorator marks the findOne method as a GraphQL query. The { name: 'note' } option specifies the name of the query. The method expects an argument named id of type Int, and it uses this.noteService.findOne(id) to retrieve a single note by ID and return it.

  • updateNote mutation: This decorator marks the updateNote method as a GraphQL mutation. The @Args('updateNoteInput') decorator specifies that the method expects an argument named updateNoteInput of type UpdateNoteInput. Inside the method, you call this.noteService.update(updateNoteInput.id, updateNoteInput) to update a note and return it.

  • removeNote mutation: This decorator marks the removeNote method as a GraphQL mutation. The @Args('id', { type: () => Int }) decorator specifies that the method expects an argument named id of type Int. Inside the method, you call this.noteService.remove(id)` to delete a note.

Module

The module is where we define the metadata for libraries we use in our API.

COPY

COPY

//note.module.ts
import { Module } from '@nestjs/common';
import { NoteService } from './note.service';
import { NoteResolver } from './note.resolver';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Note } from './entities/note.entity';

@Module({
imports: [TypeOrmModule.forFeature([Note])],
providers: [NoteResolver, NoteService],
})
export class NoteModule {}

Here add the Typeorm module to the import or the note feature.

Ensure you run the migration to create the columns in our database. In the event you have used the configuration in the suggested article then run your migration as follows:

make migration: npm run typeorm:generate-migration migrations/<name_of_migration>

run migration: npm run typeorm:run-migrations

Testing the Endpoint

One of the grand advantages of GraphQL is that we only make use of one endpoint as opposed to REST here we have multiple endpoints as per the operations you want to have for your data.

Now that the feature is complete let us proceed to test the endpoint. We shall make use of the GraphQL playground at http://localhost:3000/graphql

After running the npm run start command, you will see the schema auto-generated. It will look like the one below.

COPY

COPY

//schema.gql

------------------------------------------------------

THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY)

------------------------------------------------------

input CreateNoteInput {
body: String!
title: String!
}

type Mutation {
createNote(createNoteInput: CreateNoteInput!): Note!
removeNote(id: Int!): Note!
updateNote(updateNoteInput: UpdateNoteInput!): Note!
}

type Note {
body: String!
id: Int!
title: String!
}

type Query {
note(id: Int!): Note!
notes: [Note!]!
}

input UpdateNoteInput {
body: String!
id: Float!
title: String!
}

Create note

To test the creation of a note ensure you check the schema for the format of the the request. According to our schema, the mutation should look like the one below. Add the mutation to the left panel and click the 'play' button.

COPY

COPY

//create mutation
mutation {
createNote(createNoteInput: {
title: "Test",
body: "Test body"
}) {
id
title
body
}
}

Create note

Find one note

To test the fetching of a note ensure you check the schema for the format of the the request. According to our schema, the query should look like the one below. Add the query to the left panel and click the 'play' button.

COPY

COPY

//find one query
query {
note(id:1) {
id,
title,
body
}
}

Find all

Find all notes

To test the fetching of all notes ensure you check the schema for the format of the the request. According to our schema, the query should look like the one below. Add the query to the left panel and click the 'play' button.

COPY

COPY

//find all query
query {
notes{
id,
title,
body
}
}

All notes

Update note

To test the updating of a note ensure you check the schema for the format of the request. According to our schema, the mutation should look like the one below. Add the mutation to the left panel and click the 'play' button.

COPY

COPY

// update mutation
mutation {
updateNote(updateNoteInput: {
id: 1,
title: "Updated Title",
body: "Updated body"
}) {
id
title
body
}
}

Update notes

Delete note

To test the deletion of a note ensure you check the schema for the format of the the request. According to our schema, the mutation should look like the one below. Add the mutation to the left panel and click the 'play' button.

COPY

COPY

 //delete mutation
mutation {
removeNote(id: 1) {
id
title
body
}
}

Remove notes

The link to the project used in this tutorial is here. Feel free to try out and comment your thoughts. Until next time may the code be with you.

References