How to build a GraphQL API with NestJS - LogRocket Blog
NestJS is a TypeScript Node.js framework that helps you build enterprise-grade, efficient, and scalable Node.js applications. It supports both RESTful and GraphQL API design methodologies.
In a previous post, we covered how to build RESTful APIs using NestJS. In this tutorial, we’ll demonstrate how to use NestJS to harness the power of GraphQL APIs.
GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data. It provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and facilitates the use of powerful developer tools.
Nest offers two methods of building GraphQL APIs: code-first and schema-first. The code-first approach involves using TypeScript classes and decorators to generate GraphQL schemas. With this approach, you can reuse your data model class as a schema and decorate it with the @ObjectType()
decorator and Nest will autogenerate the schema from your model. The schema-first approach involves defining the schema using GraphQL’s Schema Definition Language(SDL) and then implementing a service by matching the definitions in the schema.
Setup
Installing NestJS
Starting a Nest project is simple. Nest provides a CLI that you can use to generate a new project. If you have npm installed, you can create a new Nest project with the following commands.
npm i -g @nestjs/cli
nest new project-name
Nest will create a project directory using project-name
and add some boilerplate files.
Adding GraphQL
Under the hood, Nest uses the Apollo GraphQL server implementation to use GraphQL with Nest applications. To add APIs to our Nest project, we need to install Apollo Server and other GraphQL dependencies.
$ npm i --save @nestjs/graphql graphql-tools graphql apollo-server-express
With the dependencies installed, you can now import the GraphQLModule
into the AppModule
.
//src/app.module.ts
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
@Module({
imports: [
GraphQLModule.forRoot({}),
],
})
export class AppModule {}
GraphQLModule
is a wrapper over Apollo Server. It provides a static method, forRoot()
, for configuring the underlying Apollo instance. The forRoot()
method accepts a list of options that is passed into the Apollo server constructor.
Click here to see the full demo with network requests
Adding database
Nest is database-agnostic; it allows integration with any database, Object Document Mapper (ODM), or Object Relational Mapper (ORM). For the purpose of this guide, we’ll use PostgreSQL and TypeORM. The Nest team recommends using TypeORM with Nest because it’s the most mature ORM available for TypeScript. Since it’s written in TypeScript, it integrates well with the Nest framework. Nest provides the @nestjs/typeorm
package for working with TypeORM.
Let’s install these dependencies
$ npm install --save @nestjs/typeorm typeorm pg
Once the installation process is complete, we can connect to the database using the TypeOrmModule
.
// src/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { GraphQLModule } from '@nestjs/graphql';
import { TypeOrmModule } from '@nestjs/typeorm'
@Module({
imports: [
GraphQLModule.forRoot({
autoSchemaFile: 'schema.gql'
}),
TypeOrmModule.forRoot({
type: 'postgres',
host: 'localhost',
port: 5432,
username: 'godwinekuma',
password: '',
database: 'invoiceapp',
entities: ['dist/**/*.model.js'],
synchronize: false,
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule { }
Resolvers
Resolvers provide instructions for turning a GraphQL operation (a query, mutation, or subscription) into data. They either return the type of data we specify in our schema or a promise for that data. The @nestjs/graphql
package automatically generates a resolver map using the metadata provided by the decorators used to annotate classes. To demonstrate how to use the package features to create a GraphQL API, we’ll create a simple invoice API.
To building our API, we’ll use the code-first approach.
Object types
Object types are the most basic components of a GraphQL schema. It is a collection of fields that you can fetch from your service, with each field declaring a type. Each object type defined represents a domain object in your API. For example, our sample invoice API needs to be able to fetch a list of customers and their invoices, so we should define the Customer
and Invoice
object types to support this functionality.
Since we’re using the code-first approach, we’ll define schemas using TypeScript classes and then use TypeScript decorators to annotate the fields of those classes.
// src/invoice/customer.model.ts
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, OneToMany } from 'typeorm';
import { ObjectType, Field } from '@nestjs/graphql';
import { InvoiceModel } from '../invoice/invoice.model';
@ObjectType()
@Entity()
export class CustomerModel {
@Field()
@PrimaryGeneratedColumn('uuid')
id: string;
@Field()
@Column({ length: 500, nullable: false })
name: string;
@Field()
@Column('text', { nullable: false })
email: string;
@Field()
@Column('varchar', { length: 15 })
phone: string;
@Field()
@Column('text')
address: string;
@Field(type => [InvoiceModel], { nullable: true })
@OneToMany(type => InvoiceModel, invoice => invoice.customer)
invoices: InvoiceModel[]
@Field()
@Column()
@CreateDateColumn()
created_at: Date;
@Field()
@Column()
@UpdateDateColumn()
updated_at: Date;
}
Notice that we decorated the class with the @ObjectTpye()
from @nestjs/graphql
. The decorator tells Nest that the class is an object class.
Field
Each property in our CustomerModel
class above is decorated with the @Field()
decorator. Nest requires us to explicitly use the @Field()
decorator in our schema definition classes to provide metadata about each field’s GraphQL type and optionality.
A field’s GraphQL type can be either a scalar type or another object type. GraphQL comes with a set of default scalar types out of the box: Int
, String
, ID
, Float
, and Boolean
. The @Field()
decorator accepts an optional type function (e.g., type => Int) and optionally an options object.
When the field is an array, we must manually indicate the array type in the @Field()
decorator’s type function, as shown below.
@Field(type => [InvoiceModel])
invoices: InvoiceModel[]
Now that we’ve created the CustomerModel
object type, let’s define the InvoiceModel
object type.
// src/invoice/invoice.model.ts
import { CustomerModel } from './../customer/customer.model';
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, JoinColumn, ManyToOne, ChildEntity } from 'typeorm';
import { ObjectType, Field } from '@nestjs/graphql';
export enum Currency {
NGN = "NGN",
USD = "USD",
GBP = "GBP",
EUR = " EUR"
}
export enum PaymentStatus {
PAID = "PAID",
NOT_PAID = "NOT_PAID",
}
@ObjectType()
export class Item{
@Field()
description: string;
@Field()
rate: number;
@Field()
quantity: number
}
@ObjectType()
@Entity()
export class InvoiceModel {
@Field()
@PrimaryGeneratedColumn('uuid')
id: string;
@Field()
@Column({ length: 500, nullable: false })
invoiceNo: string;
@Field()
@Column('text')
description: string;
@Field(type => CustomerModel)
@ManyToOne(type => CustomerModel, customer => customer.invoices)
customer: CustomerModel;
@Field()
@Column({
type: "enum",
enum: PaymentStatus,
default: PaymentStatus.NOT_PAID
})
paymentStatus: PaymentStatus;
@Field()
@Column({
type: "enum",
enum: Currency,
default: Currency.USD
})
currency: Currency;
@Field()
@Column()
taxRate: number;
@Field()
@Column()
issueDate: string;
@Field()
@Column()
dueDate: string;
@Field()
@Column('text')
note: string;
@Field( type => [Item])
@Column({
type: 'jsonb',
array: false,
default: [],
nullable: false,
})
items: Item[];
@Column()
@Field()
taxAmount: number;
@Column()
@Field()
subTotal: number;
@Column()
@Field()
total: string;
@Column({
default: 0
})
@Field()
amountPaid: number;
@Column()
@Field()
outstandingBalance: number;
@Field()
@Column()
@CreateDateColumn()
createdAt: Date;
@Field()
@Column()
@UpdateDateColumn()
updatedAt: Date;
}
GraphQL special object types
We’ve seen how to define object types with Nest. However, there are two special types in GraphQL: Query
and Mutation
. These serve as parents to other object types and are special because they define the entry point to other objects. Every GraphQL API has a Query
type and may or may not have a Mutation
type.
Our invoice API should have a query like the one below.
type Query {
customer: CustomerModel
invoice: InvoiceModel
}
Having created the objects that should exist in our graph, we can now define our resolver class to give our client a way to interact with our API. In the code-first method, a resolver class both defines resolver functions and generates the Query
type. To create a resolver, we’ll create a class with resolver functions as methods and decorate the class with the @Resolver()
decorator.
//src/customer/customer.resolver.ts
import { InvoiceModel } from './../invoice/invoice.model';
import { InvoiceService } from './../invoice/invoice.service';
import { CustomerService } from './customer.service';
import { CustomerModel } from './customer.model';
import { Resolver, Mutation, Args, Query, ResolveField, Parent } from '@nestjs/graphql';
import { Inject } from '@nestjs/common';
@Resolver(of => CustomerModel)
export class CustomerResolver {
constructor(
@Inject(CustomerService) private customerService: CustomerService,
@Inject(InvoiceService) private invoiceService: InvoiceService
) { }
@Query(returns => CustomerModel)
async customer(@Args('id') id: string): Promise<CustomerModel> {
return await this.customerService.findOne(id);
}
@ResolveField(returns => [InvoiceModel])
async invoices(@Parent() customer) {
const { id } = customer;
console.log(customer);
return this.invoiceService.findByCustomer(id);
}
@Query(returns => [CustomerModel])
async customers(): Promise<CustomerModel[]> {
return await this.customerService.findAll();
}
}
The @Resolver()
decorator accepts an optional argument that is used to specify the parent of a field resolver function. In our example above, we created the CustomerResolver
, which defines one query resolver function and one field resolver function. To specify that the method is a query handler, we annotated the method with the @Query()
decorator. We used @ResolveField()
to annotate the method that resolves the invoices
field of the CustomerModel
. The @Args()
decorator is used to extract arguments from a request for use in the query handler.
GraphQL playground
Now that we’ve created an entry point to our graph service, we can view our GraphQL API via the playground. The playground is a graphical, interactive, in-browser GraphQL IDE, available by default on the same URL as the GraphQL server itself. To access the playground, we need to have our GraphQL server running.
Run the following command to start the server.
npm start
With the server running, open your web browser and navigate to http://localhost:3000/graphql
to see the playground.
Mutations
We’ve walked through how to retrieve data from a GraphQL server, but what about modifying the server-side data? Mutation methods are used for modifying server-side data in GraphQL.
Technically, a Query
could be implemented to add server-side data. But the common convention is to annotate any method that causes data to write with the @Mutations()
decorator. The decorator tells Nest that such a method is for data modification.
Let’s add the new createCustomer()
to our resolver.
@Mutation(returns => CustomerModel)
async createCustomer(
@Args('name') name: string,
@Args('email') email: string,
@Args('phone', { nullable: true }) phone: string,
@Args('address', { nullable: true }) address: string,
): Promise<CustomerModel> {
return await this.customerService.create({ name, email, phone, address })
}
createCustomer()
has been decorated with @Mutations()
to indicate that it modifies or adds new data.
If a mutation needs to take an object as an argument, we would need to create a special kind of object called InputType
and then pass in as an argument to the method. To declare an input type, use the @InputType()
decorator.
import { PaymentStatus, Currency, Item } from "./invoice.model";
import { InputType, Field } from "@nestjs/graphql";
@InputType()
class ItemDTO{
@Field()
description: string;
@Field()
rate: number;
@Field()
quantity: number
}
@InputType()
export class CreateInvoiceDTO{
@Field()
customer: string;
@Field()
invoiceNo: string;
@Field()
paymentStatus: PaymentStatus;
@Field()
description: string;
@Field()
currency: Currency;
@Field()
taxRate: number;
@Field()
issueDate: Date;
@Field()
dueDate: Date;
@Field()
note: string;
@Field(type => [ItemDTO])
items: Array<{ description: string; rate: number; quantity: number }>;
}
@Mutation(returns => InvoiceModel)
async createInvoice(
@Args('invoice') invoice: CreateInvoiceDTO,
): Promise<InvoiceModel> {
return await this.invoiceService.create(invoice)
}
Conclusion
We demonstrated how to use the code-first approach to building a GraphQL API with Nest. You can find the complete version of the sample code shared here can on GitHub.
To learn more about the schema-first approach and other best practices, check out the Nest documentation.
First Published: Logrocket Blog