How to Use GraphQL with Nestjs

  • How to Use GraphQL with Nestjs

    Sharing is Caring... Show some love :)

    GraphQL is an open-source data query and manipulation language for APIs and a runtime query engine. Facebook (now Meta) started GraphQL development in 2012 and released it in 2015.

    It was moved in November 2018 to the newly established GraphQL Foundation, hosted by the non-profit Linux Foundation.

    Setup Nestjs Application

    To ensure that your computer is ready to use Nestjs, make sure you have set up node.js (version 12 or 14) and npm properly. If you are new to Nestjs, I suggest reading the ultimate guide to Nestjs before using the Nestjs CLI to quickly set up your project.

    npm i -g @nestjs/cli
    nest new project-name

    The above code snippet creates a project directory with “project-name” in your local machine, open your vs code and let’s start coding.

    Running the Application

    Then proceed to run the command line below in your terminal

    npm run start:dev

    Quick Start

    To begin, you’ll need to install the necessary packages. Open your terminal and execute the following command:

    
    npm i @nestjs/graphql @nestjs/apollo graphql apollo-server-express
    npm i --save class-validator class-transformer
    npm install prisma --save-dev
    npm install @prisma/client
    npm i --save @nestjs/config

    There are two approaches to creating GraphQL APIs in Nestjs: the code-first method and the schema-first method. With the code-first approach, TypeScript and decorators are used to generate the schema automatically.

    A portfolio builder for tech writers

    On the other hand, the schema-first method generates TypeScript definitions from GraphQL schemas, with the GraphQL SDL (Schema Definition Language) files serving as the source of truth. For this article, we will be using the code-first method.

    Configuration

    After installing the packages mentioned above, you can proceed to configure the GraphQL module and Config service in your Nestjs application by editing the app.module.ts file. The following code snippet illustrates how to do this:

    import { Module } from '@nestjs/common';
    import { GraphQLModule } from '@nestjs/graphql';
    import { ConfigModule } from '@nestjs/config';
    import { join } from 'path';
    import { AppController } from './app.controller';
    import { AppService } from './app.service';
    import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
    
    @Module({
      imports: [
        GraphQLModule.forRoot<ApolloDriverConfig>({
          driver: ApolloDriver,
          debug: true,
          playground: true,
          autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
        }),
        ConfigModule.forRoot({ isGlobal: true }),
      ],
     controllers: [AppController],
     providers: [AppService],
    })
    export class AppModule {}

    Open your browser, navigate to “http://localhost:3000/graphql“, and you’ll see a screen like this.

    The image shown above is known as the GraphQL Playground. It is a web-based GraphQL IDE that can be used to query and test the APIs you have created.

    Endpoints

    In this article, we will be using the code-first approach to develop a CRUD API with GraphQL. Our book app will enable users to create, read, update, and delete both authors and books. To interface with the MongoDB database used by the application, we will rely on Prisma.

    Prisma Service

    Next, we’ll configure and set up the Prisma service inside our NestJS application. To do so, run the following commands:

    npx prisma init

    The above command creates the Prisma directory, which contains the following

    • schema.prisma: specifies the database connection and database schema
    • env: specifies the database credentials used for the database connection

    Database connection

    Next, we’ll navigate to the schema.prisma file generated in our directory and configure the database to reference our MongoDB credentials.

    generator client {
      provider = "prisma-client-js"
    }
    
    datasource db {
      provider = "mongodb"
      url      = env("DATABASE_URL")
    }

    Model

    Continuing with the schema.prisma file, we’ll establish models for both authors and books.

    generator client {
      provider = "prisma-client-js"
    }
    
    datasource db {
      provider = "mongodb"
      url      = env("DATABASE_URL")
    }
    
    model Author {
      id      String   @id @default(auto()) @map("_id") @db.ObjectId
      email   String   @unique
      firstName    String
      lastName    String
      address   String?
      books   Book[]
    }
    
    model Book {
      id       String    @id @default(auto()) @map("_id") @db.ObjectId
      title    String    @unique
      description     String
      author   Author?    @relation(fields: [authorId], references: [id])
      authorId String?    @db.ObjectId
    }

    In our environment file, we’ll include the following code to pass our MongoDB connection string:

    DATABASE_URL="mongodb+srv://test:[email protected]/myFirstDatabase"

    In the terminal, we’ll create a Prisma service using the NestJS CLI as demonstrated below:

    nest -g prisma module --no-spec
    nest -g prisma service --no-spec

    Running the command above generates a Prisma folder inside our src folder, which contains the prisma.service.ts and prisma.module.ts files. To configure the Prisma service, we’ll open the “prisma.service.ts” file and set it up as follows:

    
    import { Injectable } from '@nestjs/common';
    import { ConfigService } from '@nestjs/config';
    import { PrismaClient } from '@prisma/client';
    
    @Injectable()
    export class PrismaService extends PrismaClient {
      constructor(private config: ConfigService) {
        super({
          datasources: {
            db: {
              url: config.get<string>('DATABASE_URL'),
            },
          },
        });
      }
    }

    Grow your technical writing career in one place.

    The main purpose of creating a Prisma service is to facilitate the persistence of the Prisma client and database connection, enabling seamless interaction with the database whenever required.

    ALSO READ  Top 10 Database Clients For Developers

    To achieve this, we need to import the Prisma module in our app.module.ts file, which can be done as shown below.

    
    import { Module } from '@nestjs/common';
    import { GraphQLModule } from '@nestjs/graphql';
    import { ConfigModule } from '@nestjs/config';
    import { join } from 'path';
    import { AppController } from './app.controller';
    import { AppService } from './app.service';
    import { PrismaModule } from './prisma/prisma.module';
    import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
    
    @Module({
      imports: [
        GraphQLModule.forRoot<ApolloDriverConfig>({
          driver: ApolloDriver,
          debug: true,
          playground: true,
          autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
        }),
        ConfigModule.forRoot({ isGlobal: true }),
        PrismaModule,
      ],
     controllers: [AppController],
     providers: [AppService],
    })
    export class AppModule {}

    CRUD Operation

    Let’s now explore how to build our GraphQL CRUD APIs. To begin, we’ll generate boilerplate code for authors and books using the following Nest commands:

    nest -g module authors --no-spec
    nest -g service authors --no-spec
    nest -g resolver authors --no-spec
    nest -g module books --no-spec
    nest -g service books --no-spec
    nest -g resolver books --no-spec

    After executing the commands mentioned above, the authors and books folders will be created, along with several files, including authors.module.ts, books.module.ts, authors.service.ts, books.service.ts, authors.resolver.ts, and books.resolver.ts.

    Resolvers play a crucial role in GraphQL operations as they provide instructions for converting a query, mutation, or subscription into data. These resolvers return data in the same shape as specified in our schema, either synchronously or as a promise that resolves to a result of that shape.

    Next, we will create two new folders inside the authors and books folders respectively called dto and models. We should name the files author.dto.ts, author.model.ts, book.dto.ts, and book.model.ts. The following code snippets demonstrate the appropriate file names:

    
    import { Field, InputType } from '@nestjs/graphql';
    import {
      IsAlpha,
      IsEmail,
      IsNotEmpty,
      IsString,
      Matches,
    } from 'class-validator';
    import { BookDto } from 'src/book/dto/book.dto';
    
    @InputType()
    export class AuthorDto {
      @IsAlpha()
      @IsNotEmpty()
      @Field()
      firstName: string;
    
      @IsAlpha()
      @IsNotEmpty()
      @Field()
      lastName: string;
    
      @IsAlpha()
      @IsString()
      @Field()
      address?: string;
    
      @IsNotEmpty()
      @Matches(
        /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/,
        { message: 'email must be a valid email' },
      )
      @Field()
      @IsEmail()
      email: string;
    
      @Field(() => [BookDto], { nullable: true })
      books?: [BookDto];
    }
    
    import { Field, ID, ObjectType } from '@nestjs/graphql';
    import { IsAlpha, IsEmail, IsString } from 'class-validator';
    import { Book } from 'src/book/models/book.model';
    
    @ObjectType()
    export class Author {
      @Field(() => ID, { nullable: true })
      id: number;
    
      @Field(() => String, { nullable: true })
      @IsAlpha()
      firstName: string;
    
      @Field(() => String, { nullable: true })
      @IsAlpha()
      lastName: string;
    
      @Field(() => String, { nullable: true })
      @IsEmail()
      email: string;
    
      @Field(() => String, { nullable: true })
      @IsString()
      address?: string;
    
      @Field(() => [Book], { nullable: true })
      books?: [Book];
    }
    
    import { Field, InputType } from '@nestjs/graphql';
    import { IsAlpha, IsNotEmpty } from 'class-validator';
    
    @InputType()
    export class BookDto {
      @IsAlpha()
      @IsNotEmpty()
      @Field()
      title: string;
    
      @IsAlpha()
      @IsNotEmpty()
      @Field({ nullable: true })
      description: string;
    }
    
    import { Field, ID, ObjectType } from '@nestjs/graphql';
    import { IsAlpha, IsString } from 'class-validator';
    import { Author } from 'src/author/models/author.model';
    
    @ObjectType()
    export class Book {
      @Field(() => ID, { nullable: true })
      id: number;
    
      @Field(() => String, { nullable: true })
      @IsAlpha()
      @Field()
      title: string;
    
      @Field(() => String, { nullable: true })
      @IsAlpha()
      description?: string;
    
      @Field(() => String, { nullable: true })
      @IsString()
      authorId: string;
    
      @Field(() => Author, { nullable: true })
      author?: Author;
    }

    Services

    Next, we can proceed to the service files located in the authors and books folders and write the necessary database queries and other logic.

    ALSO READ  Laravel Many to Many Relationships with CRUD Example

    Author service

    To begin creating API services for authors, open the author.service.ts file located in the author folder.

    In this file, we will define the necessary functions for creating an author, finding authors, finding a specific author, updating an author, and deleting an author.

    Create author

    import { ForbiddenException, Injectable } from '@nestjs/common';
    import {
      PrismaClientKnownRequestError,
      PrismaClientValidationError,
    } from '@prisma/client/runtime';
    import { PrismaService } from 'src/prisma/prisma.service';
    import { AuthorDto } from './dto/author.dto';
    
    @Injectable()
    export class AuthorService {
     constructor(private prisma: PrismaService) {}
    async createAuthor(authorDto: AuthorDto): Promise<any> {
        await this.prisma.$connect();
        //   check if author exists
        const checkIfAuthorExist = await this.prisma.author.findUnique({
          where: {
            email: authorDto.email,
          },
        });
    
        if (checkIfAuthorExist) {
          throw new ForbiddenException('Author with these credentials exists');
        }
        const bookData = authorDto.books?.map((book) => {
          return {
            title: book.title,
            description: book.description || null,
          };
        });
    
        try {
          const author = await this.prisma.author.create({
            data: {
              fistName: authorDto.firstName,
              lastName: authorDto.lastName,
              email: authorDto.email,
              address: authorDto.address,
              books: {
                create: bookData,
              },
            },
            include: { books: true },
          });
          return author;
        } catch (error) {
          if (error instanceof PrismaClientKnownRequestError) {
            throw new ForbiddenException(
              'Author with these credentials already exist',
            );
          } else if (error instanceof PrismaClientValidationError) {
            throw new ForbiddenException('Invalid credentials');
          } else {
            throw error;
          }
        }
      }
    }

    Get Authors

    async findAuthors() {
        try {
          const authors = await this.prisma.author.findMany({
            include: { books: true },
          });
          return authors;
        } catch (error) {
          if (error instanceof PrismaClientKnownRequestError) {
            throw new ForbiddenException('No Data');
          } else {
            throw error;
          }
        }
      }

    Get a single Author

    async findAuthor(id: string) {
        try {
          const author = await this.prisma.author.findUnique({
            where: { id: id },
            include: { books: true },
          });
          return author;
        } catch (error) {
          if (error instanceof PrismaClientKnownRequestError) {
            throw new ForbiddenException('No Data');
          } else {
            throw error;
          }
        }
      }

    Update Author

    
    async updateAuthor(authorDto: UpdateAuthorDto, id: string): Promise<any> {
        await this.prisma.$connect();
        try {
          const author = await this.prisma.author.update({
            where: { id: id },
            data: {
              fistName: authorDto.firstName,
              lastName: authorDto.lastName,
              address: authorDto.address,
            },
          });
          return author;
        } catch (error) {
          if (error instanceof PrismaClientKnownRequestError) {
            throw new ForbiddenException(
              'Author with these credentials already exist',
            );
          } else if (error instanceof PrismaClientValidationError) {
            throw new ForbiddenException('Invalid credentials');
          } else {
            throw error;
          }
        }
      }

    Delete Author

    
    async deleteAuthor(id: string): Promise<any> {
        try {
          const removeAuthor = this.prisma.author.delete({
            where: { id: id },
          });
          return removeAuthor;
        } catch (error) {
          if (error instanceof PrismaClientKnownRequestError) {
            throw new ForbiddenException(
              'Inavlid Author credentials ',
            );
          } else if (error instanceof PrismaClientValidationError) {
            throw new ForbiddenException('Invalid credentials');
          } else {
            throw error;
          }
        }
      }

    Book Service

    To create API services for books, open the book.service.ts file located in the book folder.

    In this file, we will define functions for creating a book, finding books, finding a specific book, updating a book, and deleting a book.

    Create Book

    async createBook(bookDto: BookDto, authorId: string): Promise<any> {
        await this.prisma.$connect();
        const checkIfBookExist = await this.prisma.book.findUnique({
          where: {
            title: bookDto.title,
          },
        });
    
        if (checkIfBookExist) {
          throw new ForbiddenException('Book exists');
        }
    
        const checkIfAuthorExist = await this.prisma.author.findUnique({
          where: {
            id: authorId,
          },
        });
    
        if (!checkIfAuthorExist) {
          throw new ForbiddenException('Invalid Author Id');
        }
    
        try {
          const book = await this.prisma.book.create({
            data: {
              title: bookDto.title,
              description: bookDto.description,
              author: { connect: { id: authorId } },
            },
            include: { author: true },
          });
    
          return book;
        } catch (error) {
          if (error instanceof PrismaClientKnownRequestError) {
            throw new ForbiddenException(
              'Book with these credentials already exist',
            );
          } else if (error instanceof PrismaClientValidationError) {
            throw new ForbiddenException('Invalid credentials');
          } else {
            throw error;
          }
        }
      }

    Get Books

    async findBooks() {
        try {
          const books = this.prisma.book.findMany({
            include: { author: true },
          });
          return books;
        } catch (error) {
          if (error instanceof PrismaClientKnownRequestError) {
            throw new ForbiddenException('No Data');
          } else {
            throw error;
          }
        }
      }

    Get a single book

    async findBook(id: string) {
        try {
          const book = await this.prisma.book.findUnique({
            where: { id: id },
            include: { author: true },
          });
          return book;
        } catch (error) {
          if (error instanceof PrismaClientKnownRequestError) {
            throw new ForbiddenException('No Data');
          } else {
            throw error;
          }
        }
      }

    Delete Book

    async deleteBook(id: string): Promise<any> {
        try {
          const removeBook = this.prisma.book.delete({
            where: { id: id },
          });
          return removeBook;
        } catch (error) {
          if (error instanceof PrismaClientKnownRequestError) {
            throw new ForbiddenException(
              'Book with these credentials already exist',
            );
          } else if (error instanceof PrismaClientValidationError) {
            throw new ForbiddenException('Invalid credentials');
          } else {
            throw error;
          }
        }
      }

    Resolvers

    As mentioned earlier, resolvers play a vital role in converting GraphQL operations into data. We can now proceed to the author and book resolver files to specify the necessary GraphQL instructions.

    ALSO READ  MongoDB Tutorial: The Ultimate Guide (2022)

    Author Resolver

    You can find the author.resolver.ts file inside the author folder. To implement the necessary GraphQL instructions, we can populate this file with the following code snippets.

    import { Args, Context, ID, Mutation, Query, Resolver } from '@nestjs/graphql';
    import { AuthorService } from './author.service';
    import { AuthorDto } from './dto/author.dto';
    import { UpdateAuthorDto } from './dto/update-author.dto';
    import { Author } from './models/author.model';
    
    @Resolver(Author)
    export class AuthorResolver {
      constructor(private authorsService: AuthorService) {}
    
      @Mutation(() => Author)
      async createAuthor(
        @Args('authorDto') authorDto: AuthorDto,
        @Context() ctx,
      ): Promise<Author> {
        return this.authorsService.createAuthor(authorDto);
      }
    
      // FIND ALL AUTHORS
      @Query(() => [Author], { nullable: true })
      async authors(@Context() ctx) {
        return this.authorsService.findAuthors();
      }
    
      // FIND A SINGLE AUTHOR
      @Query(() => Author, { nullable: true })
      async author(@Args('id', { type: () => ID }) id: string, @Context() ctx) {
        return this.authorsService.findAuthor(id);
      }
    
      // UPDATE AUTHOR RECORD
      @Mutation(() => Author)
      async editAuthor(
        @Args('authorDto') authorDto: UpdateAuthorDto,
        @Args('id', { type: () => ID }) id: string,
        @Context()
        ctx,
      ): Promise<Author> {
        return this.authorsService.updateAuthor(authorDto, id);
      }
    
      // DELETE AUTHOR RECORD
      @Mutation(() => Author)
      async deleteAuthor(
        @Args('id', { type: () => ID }) id: string,
        @Context()
        ctx,
      ) {
        return this.authorsService.deleteAuthor(id);
      }
    }

    Book Resolver

    import { Args, Context, ID, Mutation, Query, Resolver } from '@nestjs/graphql';
    import { BookService } from './book.service';
    import { BookDto } from './dto/book.dto';
    import { Book } from './models/book.model';
    
    @Resolver(Book)
    export class BookResolver {
      constructor(private bookService: BookService) {}
    
      // CREATE BOOK VIA PRISMA SERVICE
      @Mutation(() => Book)
      async createBook(
        @Args('bookDto') bookDto: BookDto,
        @Args('authorId') authorId: string,
        @Context() ctx,
      ): Promise<Book> {
        return this.bookService.createBook(bookDto, authorId);
      }
    
      // FIND ALL BOOKS VIA PRISMA SERVICE
      @Query(() => [Book], { nullable: true })
      async books(@Context() ctx) {
        return this.bookService.findBooks();
      }
    
      // FIND A BOOK VIA PRISMA SERVICE
      @Query(() => Book, { nullable: true })
      async book(@Args('id', { type: () => ID }) id: string, @Context() ctx) {
        return this.bookService.findBook(id);
      }
    
      // DELETE BOOK VIA PRISMA SERVICE
      @Mutation(() => Book)
      async deleteBook(
        @Args('id', { type: () => ID }) id: string,
        @Context()
        ctx,
      ) {
        return this.bookService.deleteBook(id);
      }
    }

    We have now created the necessary components for our GraphQL APIs, including the author service, author resolver, book service, and book resolver.

    To test our APIs, we can open our browser and enter “http://localhost:3000/graphql” in the address bar.

    This should bring up a screen similar to the image below:

    On the right side of the screen, you will see the docs and schema options. By clicking on docs, you can view a list of all the queries and mutations that we have defined in our code.

    Clicking on schema will display a list of the GraphQL schema definitions, as shown in the image below.

    It’s worth noting that the same schema definitions can also be found in the schema.gql file.

    However, you should avoid editing the contents of this file.

    Conclusion

    So far we have:

    1. Learned how to set up a nest js project using Nestjs CLI.
    2. Learned about the 2 approaches to GraphQL in the Nestjs eco-system.
    3. Configure the GraphQL module and Config service in the root of our application.
    4. Learned about the GraphQL playground which is essentially a tool for interacting with our GraphQL APIs.
    5. Implement a Prisma Service for persisting the database.

    If you like my content you can connect with me on LinkedIn and on Twitter.

    Thanks for reading.

    Ready to ditch Google Drive? Try Contentre.

    Start Learning Backend Dev. Now

    Stop waiting and start learning! Get my 10 tips on teaching yourself backend development.

    Don't worry. I'll never, ever spam you!

    Sharing is caring :)

    Coding is not enough
    Learning for all. Savings for you. Courses from $11.99

    Comments