GraphQL and NestJS make an excellent partnership, giving you a solid foundation for your APIs and an easy-to-use framework to build scalable web applications. The combination is perfect for building production-ready apps, and both are very relevant tools in today’s tech ecosystem.
Find out more about how you can build an API using both products.
What Is GraphQL?
GraphQL is a data query and manipulation language you can use to build APIs in a more precise and concise way. GraphQL provides a complete and adequate description of the data existing in an API and gives power to the client to get the exact data needed.
GraphQL provides many features that REST APIs lack, ranging from precise data queries to better developer tooling, like the graphiql editor. It also allows you to query for multiple resources via a single request.
What Is NestJS?
NestJS is a progressive Node.js framework you can use to build scalable and efficient server-side applications. NestJS provides many plugins, alongside tooling for fast and easy development including GraphQL support, GRPC, WebSockets, etc.
NestJS is well-known in the ecosystem for its optimized project structure using modules, controllers, services, and schemas. Its built-in CLI allows you to create a structured API architecture. You can use dependency injection principles to control how the parts of an application communicate with each other.
Implementing GraphQL With NestJS and MongoDB
Before building an API with NestJS and GraphQL, you’ll need to have the right dependencies available. You need to install Node.js and NestJS, which you can install by running npm i -g @nestjs/cli.
The example that follows is a simple app that stores info about books. Run the following command in your terminal to create a new NestJS application:
nest new <app-name>
Navigate to the generated application's directory (<app-name>) and install its dependencies with the following command:
$ npm install --save @nestjs/config @nestjs/graphql graphql-tools graphql \
@nestjs/apollo apollo-server-express @nestjs/mongoose @types/graphql
There are two major approaches to building GraphQL APIs, namely:
- Schema-first approach: where you describe the API in schema definition files or SDL, and NestJS generates Typescript definitions based on them.
- Code-first approach: where you define queries, mutations, and other GraphQL functionalities using Typescript classes and decorators, and NestJS generates SDL files based on them.
The following example describes how to use a code-first approach.
First, you need to initialize GraphQL in your AppModule and connect it to a MongoDB database:
// app.module.ts
import { Module } from '@nestjs/common';
import { GraphQLModule as NestGraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { join } from 'path';
import { MongooseModule } from '@nestjs/mongoose';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule, ConfigService } from '@nestjs/config';
import mongodbConfig from './config/mongodb.config';
@Module({
imports: [
ConfigModule.forRoot({
load: [mongodbConfig],
isGlobal: true
}),
NestGraphQLModule.forRootAsync<ApolloDriverConfig>({
driver: ApolloDriver,
inject: [ConfigService],
useFactory: async (configService: ConfigService) => ({
autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
installSubscriptionHandlers: true,
sortSchema: true,
playground: true,
debug: configService.get<boolean>("DEBUG"),
uploads: false,
}),
}),
MongooseModule.forRootAsync({
inject: [ConfigService],
useFactory: async (configService: ConfigService) => ({
uri: configService.get('MONGO_URI')
})
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
This module imports the GraphQLModule from @nestjs/graphql and the MongooseModule from @nestjs/mongoose which helps connect to MongoDB. The autoSchemaFile property specifies the location of the generated schema file, and the sortSchema property ensures that it orders the fields alphabetically.
Here’s what your MongoDB config file should look like:
import { registerAs } from '@nestjs/config';
/**
* Mongo database connection config
*/
export default registerAs('mongodb', () => {
const {
MONGO_URI
} = process.env;
return {
uri: `${MONGO_URI}`,
};
});
Defining the GraphQL Schema
Having set up the GraphQL and MongoDB connections, you should define GraphQL queries and mutations to generate a schema (schema.gql) file.
Writing Queries
In the code-first approach, you create a model using the ObjectType decorator. You will later transform this model into a GraphQL type.
For instance:
// book.model.ts
import { Field, ObjectType } from '@nestjs/graphql';
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';
export type BookDocument = Book & Document;
@ObjectType()
@Schema()
export class Book {
@Field()
title: string;
@Field()
author: string;
@Field()
publishedDate: boolean;
}
export const BookSchema = SchemaFactory.createForClass(Book);
GraphQL, by default, can’t use the created schemas. To make them functional, you need a resolver service that contains the functions for executing the GraphQL types. You can do so with the Resolver decorator.
// books.resolver.ts
import { Resolver, Query, Mutation, Args, ID } from '@nestjs/graphql';
import { Book } from './book.model';
import { BookService } from './books.service';
@Resolver(() => Book)
export class BookResolver {
constructor(private readonly bookService: BookService) { }
@Query(() => [Book])
async books(): Promise<Book[]> {
return this.bookService.findAll();
}
@Query(() => Book)
async book(@Args('id', { type: () => ID }) id: string): Promise<Book> {
return this.bookService.findOne(id);
}
}
You can implement the BookService,imported above, as follows:
// books.service.ts
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { Book, BookDocument } from './book.model';
@Injectable()
export class BookService {
constructor(@InjectModel(Book.name) private bookModel: Model<BookDocument>) { }
async findAll(): Promise<Book[]> {
return this.bookModel.find().exec();
}
async findOne(id: string): Promise<Book> {
return this.bookModel.findById(id).exec();
}
}
You also need to add the BookResolver to the list of providers in books.module.ts.
import { Module } from "@nestjs/common";
import { MongooseModule } from "@nestjs/mongoose";
import { BookService } from './books.service';
import { BookResolver } from './books.resolver';
import { Book, BookSchema } from './book.model';
@Module({
providers: [
BookService,
BookResolver
],
imports: [MongooseModule.forFeature([
{
name: Book.name,
schema: BookSchema,
},
]),
],
})
export class BooksModule {}
Working With Mutations
While you use a query to retrieve data in GraphQL, mutations create or update data in the database. To create mutations, you need to accept data from users. The InputType decorator, which turns a class into a GraphQL input type, comes in handy here.
// book.input.ts
import { InputType, Field } from '@nestjs/graphql';
@InputType()
export class BookInput {
@Field()
title: string;
@Field()
author: string;
@Field()
publishedDate: boolean
}
You may now update books.resolver.ts to look like this:
import { Resolver, Query, Mutation, Args, ID } from '@nestjs/graphql';
import { Book } from './book.model';
import { BookService } from './books.service';
import { BookInput } from './book.input';
@Resolver(() => Book)
export class BookResolver {
constructor(private readonly bookService: BookService) { }
@Mutation(() => Book)
async createBook(@Args('input') input: BookInput): Promise<Book> {
return this.bookService.create(input);
}
@Mutation(() => Book)
async updateBook(
@Args('id', { type: () => ID }) id: string,
@Args('input') input: BookInput,
): Promise<Book> {
return this.bookService.update(id, input);
}
@Mutation(() => Book)
async deleteBook(@Args('id', { type: () => ID }) id: string): Promise<Book> {
return this.bookService.delete(id);
}
}
And books.service.ts like this:
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { Book, BookDocument } from './book.model';
@Injectable()
export class BookService {
constructor(@InjectModel(Book.name) private bookModel: Model<BookDocument>) { }
async create(book: Book): Promise<Book> {
const newBook = new this.bookModel(book);
return newBook.save();
}
async update(id: string, book: Book): Promise<Book> {
return this.bookModel.findByIdAndUpdate(id, book, { new: true }).exec();
}
async delete(id: string): Promise<Book> {
return this.bookModel.findByIdAndDelete(id).exec();
}
}
The @Mutation decorator marks a function as a mutation type and the @Args decorator grabs any inputs passed into the function.
Finally, you should import the BooksModule into AppModule to make it functional. You should also pass the BooksModule to forRootAsync as seen below.
import { BooksModule } from './books/books.module';
/**
* other imports
*/
@Module({
imports: [
ConfigModule.forRoot({
load: [mongodbConfig],
isGlobal: true
}),
NestGraphQLModule.forRootAsync<ApolloDriverConfig>({
driver: ApolloDriver,
inject: [ConfigService],
useFactory: async (configService: ConfigService) => ({
autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
installSubscriptionHandlers: true,
sortSchema: true,
playground: true,
debug: configService.get<boolean>("DEBUG"),
uploads: false,
}),
}),
MongooseModule.forRootAsync({
inject: [ConfigService],
useFactory: async (configService: ConfigService) => ({
uri: configService.get('MONGO_URI')
})
}),
BooksModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
You can test the code by running npm run start:dev in your terminal, and your application should start successfully on port 3000.
Open localhost:3000/graphql in your browser to show the Graphiql interface where you can test queries and mutations. Here’s an example that shows a query:
And here’s an example of a mutation:
Build Efficient APIs With NestJS and GraphQL
Building a GraphQL API in NestJS with MongoDB using Mongoose involves defining a schema for the GraphQL API, a schema for the Mongoose model, a service to interact with the database, and a resolver to map GraphQL operations to service methods.
NestJS has built-in functionality for building APIs, including decorators for defining routes, guards to protect them, and middlewares for handling requests and responses. It also supports other databases like PostgreSQL, MySQL, and SQLite, as well as other GraphQL libraries like Apollo and TypeGraphQL.