Building GraphQL APIs with NestJS and MongoDB: A Comprehensive Guide

Building GraphQL APIs with NestJS and MongoDB: A Comprehensive Guide

·

6 min read

NestJS, a progressive Node.js framework, leverages TypeScript to build scalable and maintainable server-side applications. Combined with GraphQL and MongoDB, NestJS provides a powerful stack for building modern APIs. This article offers a detailed guide on building GraphQL APIs with NestJS and MongoDB, covering setup, schema definition, CRUD operations, and advanced features.

Introduction to the Stack

What is GraphQL?

GraphQL is a query language for APIs and a runtime for executing those queries. It allows clients to request specific data structures, reducing over-fetching and under-fetching compared to traditional REST APIs.

What is NestJS?

NestJS is a progressive Node.js framework for building efficient, reliable, and scalable server-side applications. It uses TypeScript and is inspired by Angular, providing a modular architecture and strong typing.

Why MongoDB?

MongoDB is a NoSQL database known for its flexibility, scalability, and ease of use. It stores data in JSON-like documents, making it a good fit for applications with varied or unstructured data.

Setting Up the Project

Prerequisites

Ensure you have the following installed:

  • Node.js: Download and install from nodejs.org.

  • NestJS CLI: Install using npm: npm install -g @nestjs/cli.

  • MongoDB: Install and run MongoDB from mongodb.com.

Initializing the NestJS Project

Create a new NestJS project:

nest new graphql-nestjs
cd graphql-nestjs

Installing Required Packages

Add the necessary packages for GraphQL, MongoDB, and Mongoose (an ODM for MongoDB):

npm install @nestjs/graphql @nestjs/mongoose mongoose graphql-tools graphql apollo-server-express

Configuring MongoDB

Create a .env file in the root of your project to store the MongoDB connection string:

MONGODB_URI=mongodb://localhost:27017/graphql-nestjs

Setting Up Mongoose

Update the AppModule to include Mongoose configuration:

import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { GraphQLModule } from '@nestjs/graphql';
import { join } from 'path';

@Module({
  imports: [
    MongooseModule.forRoot(process.env.MONGODB_URI),
    GraphQLModule.forRoot({
      autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
    }),
  ],
})
export class AppModule {}

Defining the Schema and Resolvers

Creating the Book and Author Modules

Generate modules, services, and resolvers for Book and Author:

nest generate module books
nest generate service books
nest generate resolver books

nest generate module authors
nest generate service authors
nest generate resolver authors

Defining Mongoose Schemas

Create Mongoose schemas for Book and Author:

book.schema.ts

import { Schema, Document } from 'mongoose';

export const BookSchema = new Schema({
  title: String,
  authorId: { type: Schema.Types.ObjectId, ref: 'Author' },
});

export interface Book extends Document {
  id: string;
  title: string;
  authorId: string;
}

author.schema.ts

import { Schema, Document } from 'mongoose';

export const AuthorSchema = new Schema({
  name: String,
});

export interface Author extends Document {
  id: string;
  name: string;
}

Integrating Schemas with Modules

books.module.ts

import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { BooksService } from './books.service';
import { BooksResolver } from './books.resolver';
import { BookSchema } from './schemas/book.schema';

@Module({
  imports: [MongooseModule.forFeature([{ name: 'Book', schema: BookSchema }])],
  providers: [BooksService, BooksResolver],
})
export class BooksModule {}

authors.module.ts

import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { AuthorsService } from './authors.service';
import { AuthorsResolver } from './authors.resolver';
import { AuthorSchema } from './schemas/author.schema';

@Module({
  imports: [MongooseModule.forFeature([{ name: 'Author', schema: AuthorSchema }])],
  providers: [AuthorsService, AuthorsResolver],
})
export class AuthorsModule {}

Implementing Resolvers and Services

books.service.ts

import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { Book } from './schemas/book.schema';

@Injectable()
export class BooksService {
  constructor(@InjectModel('Book') private bookModel: Model<Book>) {}

  async findAll(): Promise<Book[]> {
    return this.bookModel.find().exec();
  }

  async findOne(id: string): Promise<Book> {
    return this.bookModel.findById(id).exec();
  }

  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();
  }
}

books.resolver.ts

import { Resolver, Query, Mutation, Args } from '@nestjs/graphql';
import { BooksService } from './books.service';
import { BookType } from './dto/book.dto';
import { BookInput } from './inputs/book.input';

@Resolver(() => BookType)
export class BooksResolver {
  constructor(private readonly booksService: BooksService) {}

  @Query(() => [BookType])
  async books() {
    return this.booksService.findAll();
  }

  @Query(() => BookType)
  async book(@Args('id') id: string) {
    return this.booksService.findOne(id);
  }

  @Mutation(() => BookType)
  async createBook(@Args('input') input: BookInput) {
    return this.booksService.create(input);
  }

  @Mutation(() => BookType)
  async updateBook(@Args('id') id: string, @Args('input') input: BookInput) {
    return this.booksService.update(id, input);
  }

  @Mutation(() => BookType)
  async deleteBook(@Args('id') id: string) {
    return this.booksService.delete(id);
  }
}

authors.service.ts

import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { Author } from './schemas/author.schema';

@Injectable()
export class AuthorsService {
  constructor(@InjectModel('Author') private authorModel: Model<Author>) {}

  async findAll(): Promise<Author[]> {
    return this.authorModel.find().exec();
  }

  async findOne(id: string): Promise<Author> {
    return this.authorModel.findById(id).exec();
  }

  async create(author: Author): Promise<Author> {
    const newAuthor = new this.authorModel(author);
    return newAuthor.save();
  }

  async update(id: string, author: Author): Promise<Author> {
    return this.authorModel.findByIdAndUpdate(id, author, { new: true }).exec();
  }

  async delete(id: string): Promise<Author> {
    return this.authorModel.findByIdAndDelete(id).exec();
  }
}

authors.resolver.ts

import { Resolver, Query, Mutation, Args } from '@nestjs/graphql';
import { AuthorsService } from './authors.service';
import { AuthorType } from './dto/author.dto';
import { AuthorInput } from './inputs/author.input';

@Resolver(() => AuthorType)
export class AuthorsResolver {
  constructor(private readonly authorsService: AuthorsService) {}

  @Query(() => [AuthorType])
  async authors() {
    return this.authorsService.findAll();
  }

  @Query(() => AuthorType)
  async author(@Args('id') id: string) {
    return this.authorsService.findOne(id);
  }

  @Mutation(() => AuthorType)
  async createAuthor(@Args('input') input: AuthorInput) {
    return this.authorsService.create(input);
  }

  @Mutation(() => AuthorType)
  async updateAuthor(@Args('id') id: string, @Args('input') input: AuthorInput) {
    return this.authorsService.update(id, input);
  }

  @Mutation(() => AuthorType)
  async deleteAuthor(@Args('id') id: string) {
    return this.authorsService.delete(id);
  }
}

Defining GraphQL DTOs and Inputs

Create DTOs (Data Transfer Objects) and Input types for GraphQL.

book.dto.ts

import { ObjectType, Field, ID } from '@nestjs/graphql';
import { AuthorType } from '../../authors/dto/author.dto';

@ObjectType()
export class BookType {
  @Field(() => ID)
  id: string;

  @Field()
  title: string;

  @Field(() => AuthorType)
  author: AuthorType;
}

author.dto.ts



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

@ObjectType()
export class AuthorType {
  @Field(() => ID)
  id: string;

  @Field()
  name: string;
}

book.input.ts

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

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

  @Field()
  authorId: string;
}

author.input.ts

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

@InputType()
export class AuthorInput {
  @Field()
  name: string;
}

Running the Application

Ensure MongoDB is running, and start your NestJS application:

npm run start:dev

Open http://localhost:3000/graphql in your browser. You should see the GraphQL Playground, where you can run queries and mutations.

Example Queries and Mutations

Fetching Data

query {
  books {
    id
    title
    author {
      id
      name
    }
  }
  authors {
    id
    name
  }
}

Creating Data

mutation {
  createAuthor(input: { name: "George R.R. Martin" }) {
    id
    name
  }
}

mutation {
  createBook(input: { title: "A Game of Thrones", authorId: "author-id-here" }) {
    id
    title
  }
}

Conclusion

By combining NestJS, GraphQL, and MongoDB, you can create powerful, scalable, and flexible APIs. This guide covered the setup, schema definition, CRUD operations, and advanced features of building a GraphQL API with NestJS and MongoDB. With these tools, you can efficiently develop modern applications that meet the needs of today's complex data requirements.