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.
// 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
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.
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.
import { InputType, Field } from '@nestjs/graphql';
@InputType()
export class CreateNoteInput {
@Field()
title: string;
@Field()
body: string;
}
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.
// 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 thecreateNoteInput
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 theupdateNoteInput
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.
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 callthis.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 callthis.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 usesthis.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 callthis.noteService.remove(id)
` to delete a note.
Module
The module is where we define the metadata for libraries we use in our API.
`
//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.
//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.
//create mutation
mutation {
createNote(createNoteInput: {
title: "Test",
body: "Test body"
}) {
id
title
body
}
}
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.
//find one query
query {
note(id:1) {
id,
title,
body
}
}
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.
//find all query
query {
notes{
id,
title,
body
}
}
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.
// update mutation
mutation {
updateNote(updateNoteInput: {
id: 1,
title: "Updated Title",
body: "Updated body"
}) {
id
title
body
}
}
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.
` //delete mutation
mutation {
removeNote(id: 1) {
id
title
body
}
}
`
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.
Top comments (0)