Building GraphQL APIs with NestJS and PostgreSQL: A Comprehensive Guide

Building GraphQL APIs with NestJS and PostgreSQL: A Comprehensive Guide

·

6 min read

NestJS is a progressive Node.js framework that leverages TypeScript to build scalable and maintainable server-side applications. When combined with GraphQL and PostgreSQL, it provides a powerful stack for creating modern, efficient APIs. This article will guide you through setting up a GraphQL API with NestJS and PostgreSQL, covering installation, 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 exactly the data they need, minimizing 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 PostgreSQL?

PostgreSQL is a powerful, open-source relational database system with a strong emphasis on extensibility and standards compliance. It supports advanced data types and performance optimization features.

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.

  • PostgreSQL: Install and run PostgreSQL from postgresql.org.

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, PostgreSQL, and TypeORM (an ORM for TypeScript and JavaScript):

npm install @nestjs/graphql @nestjs/typeorm typeorm pg graphql-tools graphql apollo-server-express

Configuring PostgreSQL

Set up the PostgreSQL database and create a .env file in the root of your project to store the connection details:

DATABASE_HOST=localhost
DATABASE_PORT=5432
DATABASE_USER=your_user
DATABASE_PASSWORD=your_password
DATABASE_NAME=graphql_nestjs

Setting Up TypeORM

Update the AppModule to include TypeORM configuration:

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { GraphQLModule } from '@nestjs/graphql';
import { join } from 'path';
import { BooksModule } from './books/books.module';
import { AuthorsModule } from './authors/authors.module';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'postgres',
      host: process.env.DATABASE_HOST,
      port: parseInt(process.env.DATABASE_PORT, 10),
      username: process.env.DATABASE_USER,
      password: process.env.DATABASE_PASSWORD,
      database: process.env.DATABASE_NAME,
      entities: [__dirname + '/**/*.entity{.ts,.js}'],
      synchronize: true,
    }),
    GraphQLModule.forRoot({
      autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
    }),
    BooksModule,
    AuthorsModule,
  ],
})
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 TypeORM Entities

Create TypeORM entities for Book and Author:

book.entity.ts

import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm';
import { Author } from '../../authors/entities/author.entity';

@Entity()
export class Book {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  title: string;

  @ManyToOne(() => Author, author => author.books)
  author: Author;
}

author.entity.ts

import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm';
import { Book } from '../../books/entities/book.entity';

@Entity()
export class Author {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @OneToMany(() => Book, book => book.author)
  books: Book[];
}

Integrating Entities with Modules

books.module.ts

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { BooksService } from './books.service';
import { BooksResolver } from './books.resolver';
import { Book } from './entities/book.entity';
import { AuthorsModule } from '../authors/authors.module';

@Module({
  imports: [TypeOrmModule.forFeature([Book]), AuthorsModule],
  providers: [BooksService, BooksResolver],
})
export class BooksModule {}

authors.module.ts

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthorsService } from './authors.service';
import { AuthorsResolver } from './authors.resolver';
import { Author } from './entities/author.entity';

@Module({
  imports: [TypeOrmModule.forFeature([Author])],
  providers: [AuthorsService, AuthorsResolver],
})
export class AuthorsModule {}

Implementing Resolvers and Services

books.service.ts

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Book } from './entities/book.entity';

@Injectable()
export class BooksService {
  constructor(
    @InjectRepository(Book)
    private booksRepository: Repository<Book>,
  ) {}

  findAll(): Promise<Book[]> {
    return this.booksRepository.find({ relations: ['author'] });
  }

  findOne(id: number): Promise<Book> {
    return this.booksRepository.findOne(id, { relations: ['author'] });
  }

  create(book: Book): Promise<Book> {
    return this.booksRepository.save(book);
  }

  async update(id: number, book: Book): Promise<Book> {
    await this.booksRepository.update(id, book);
    return this.booksRepository.findOne(id, { relations: ['author'] });
  }

  async remove(id: number): Promise<void> {
    await this.booksRepository.delete(id);
  }
}

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: number) {
    return this.booksService.findOne(id);
  }

  @Mutation(() => BookType)
  async createBook(@Args('input') input: BookInput) {
    const book = new Book();
    book.title = input.title;
    book.author = { id: input.authorId } as any;
    return this.booksService.create(book);
  }

  @Mutation(() => BookType)
  async updateBook(@Args('id') id: number, @Args('input') input: BookInput) {
    const book = new Book();
    book.title = input.title;
    book.author = { id: input.authorId } as any;
    return this.booksService.update(id, book);
  }

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

authors.service.ts

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Author } from './entities/author.entity';

@Injectable()
export class AuthorsService {
  constructor(
    @InjectRepository(Author)
    private authorsRepository: Repository<Author>,
  ) {}

  findAll(): Promise<Author[]> {
    return this.authorsRepository.find({ relations: ['books'] });
  }

  findOne(id: number): Promise<Author> {
    return this.authorsRepository.findOne(id, { relations: ['books'] });
  }

  create(author: Author): Promise<Author> {
    return this.authorsRepository.save(author);
  }

  async update(id: number, author: Author): Promise<Author> {
    await this.authorsRepository.update(id, author);
    return this.authorsRepository.findOne(id, { relations: ['books'] });
  }

  async remove(id: number): Promise<void> {
    await this.authorsRepository.delete(id);
  }
}

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: number) {
    return this.authorsService.findOne(id);
  }

  @Mutation

(() => AuthorType)
  async createAuthor(@Args('input') input: AuthorInput) {
    const author = new Author();
    author.name = input.name;
    return this.authorsService.create(author);
  }

  @Mutation(() => AuthorType)
  async updateAuthor(@Args('id') id: number, @Args('input') input: AuthorInput) {
    const author = new Author();
    author.name = input.name;
    return this.authorsService.update(id, author);
  }

  @Mutation(() => AuthorType)
  async deleteAuthor(@Args('id') id: number) {
    return this.authorsService.remove(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: number;

  @Field()
  title: string;

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

author.dto.ts

import { ObjectType, Field, ID } from '@nestjs/graphql';
import { BookType } from '../../books/dto/book.dto';

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

  @Field()
  name: string;

  @Field(() => [BookType])
  books: BookType[];
}

book.input.ts

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

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

  @Field(() => ID)
  authorId: number;
}

author.input.ts

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

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

Running the Application

Ensure PostgreSQL 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
    books {
      id
      title
    }
  }
}

Creating Data

mutation {
  createAuthor(input: { name: "J.K. Rowling" }) {
    id
    name
  }
}

mutation {
  createBook(input: { title: "Harry Potter and the Sorcerer's Stone", authorId: 1 }) {
    id
    title
  }
}

Conclusion

By combining NestJS, GraphQL, and PostgreSQL, 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 PostgreSQL. With these tools, you can efficiently develop modern applications that meet the needs of today's complex data requirements.