Seu guia completo para construir APIs de sucesso e acelerar sua Transformação Digital

Como construir uma API RESTful com NestJS, Prisma e Docker

Como construir uma API RESTful com NestJS, Prisma e Docker

Existem diversas formas de construir uma API e cada uma delas pode utilizar metodologias, bibliotecas e frameworks diferentes. Neste artigo, você vai aprender a desenvolver uma com NestJS, PostgreSQL, Prisma e Docker.

Aplicações desenvolvidas em Node.js, alicerçadas em frameworks, vêm ganhando muita popularidade devido a sua eficiência e facilidade durante a construção. Dentre os frameworks mais conhecidos, podemos citar o ExpressJS, o NestJS e o fastify.

Ao final desta leitura, você será capaz de desenvolver uma API do zero utilizando o framework NestJS para Node.js, que consome um banco de dados PostgreSQL, rodado em um container local através do Docker. A interação com o banco de dados será realizada pela biblioteca Prisma.

Containers

Containers são partições lógicas, fáceis de serem transportadas por diferentes máquinas, que podem conter uma aplicação, um banco de dados ou qualquer outra coisa executável em um sistema operacional.

Seu conceito remete à ideia de uma máquina virtual, porém é mais eficiente. Ao invés de se utilizar um sistema operacional para executar outros sistemas operacionais — que por sua vez executarão outros softwares — diferentes containers podem ser executados diretamente em um único sistema operacional, poupando muito recurso de hardware.

Para melhor entendimento da diferença entre os containers e máquinas virtuais, vide a imagem abaixo:

Perceba que cada máquina virtual possui seu próprio sistema operacional, que para se manter executando consome recursos além do necessário para executar a aplicação.

O container exclui esta necessidade de ter um sistema operacional à parte, reduzindo o consumo de recurso necessário para se manter ativo.

Docker

Docker é uma empresa que oferece diversas ferramentas relacionadas com a orquestração de containers. A mais usada entre elas é a Docker Compose, que serve justamente para definir e orquestrar containers de maneira fácil.

Quando eu citar Docker mais adiante neste artigo, estou me referindo à ferramenta e não à empresa, combinado?

NestJS

O Nest é um framework que permite a criação de aplicações server-side, combinando elementos de programação orientada a objetos, programação funcional e programação reativa funcional. Por de baixo dos panos, ele faz o uso do ExpressJS e permite uma compatibilidade com diversas bibliotecas.

Construindo a aplicação Node.js

1- A primeira coisa que você precisa fazer é instalar o Node.js. Dê preferência a versão LTS, já que é a versão mais recente e estável.

2- Ao fazer a instalação do Node.js, é instalado também o npm (Node Package Manager), seu gerenciador de pacotes padrão. Utilize-o para instalar a CLI (Command-Line Interface) executando o seguinte comando em um terminal:

npm i -g @nestjs/cli

3- Utilize a CLI para inicializar o projeto Nest, criando toda a estrutura de pastas e arquivos necessários para o funcionamento correto da aplicação. Isso pode ser feito através do comando:

nest new artigo-nestjs-docker-postgres

Note que o último trecho do comando será o nome do seu projeto.

Ao executar o comando, você poderá escolher o gerenciador de pacotes que será utilizado para criar os arquivos, baixar e instalar as dependências. Caso não tenha outro gerenciador de pacotes instalado de sua preferência, escolha o npm.

4- Após terminar a execução do comando anterior, você pode iniciar a aplicação com o comando:

npm start:dev

Este comando irá executar a aplicação localmente em modo de desenvolvimento, onde caso um arquivo sofra uma alteração, ela é recarregada automaticamente. Este conceito é conhecido como hot-reload ou live-reload.

Entendendo a estrutura de um projeto Nest

Por padrão, a aplicação é executada na porta 3000. Você pode testá-la através da url http://localhost:3000. Essa configuração pode ser alterada no arquivo src/main.ts onde tem as definições do servidor. Se tudo estiver ok, você vai receber uma string na response da sua chamada GET pelo navegador:

Agora, vamos entender o que aconteceu por baixo dos panos para que você recebesse esta tela. No arquivo src/main.ts de configuração, é criada uma instância da aplicação nest baseada no AppModule.

Modules

Os módulos são responsáveis por reunir as peças da sua aplicação e podem representar um recurso da mesma. De forma resumida, você consegue reunir os controllers e os providers em um módulo.

Controllers

Os controladores são responsáveis por lidar com as requisições feitas à aplicação e com as respostas dadas ao cliente. Neles são definidas as rotas dos respectivos recursos que representam. A lógica capaz de gerar a resposta esperada é abstraída em funções que ficam nos providers.

Providers

Os provedores são um conceito do Nest, onde diversas classes podem ser tratadas como tal. Dentre elas podemos citar: services, repositories, factories, helpers entre outras. A ideia principal deste conceito é injetar essas classes como dependências, evitando o processo de instanciar uma classe toda vez que ela for ser utilizada.

De um modo geral, um provider vai cuidar do “trabalho sujo” para gerar os dados esperados, abstraindo a lógica de execução de todo o resto, separando as responsabilidades dos arquivos.

Neste caso inicial construído pelo Nest, o arquivo src/app.module tem o AppModule que reúne o controller AppController, onde sua a função getHello() é chamada durante o acesso da rota “/”. Também reúne o provider AppService, que é responsável por retornar uma string “Hello World!”, através da função com o mesmo nome de getHello().

Quando o usuário acessa a rota “/”, o módulo procura o controller responsável por lidar com essa rota. Neste caso é o AppController, com a função getHello(). Esta função chama a função getHello() que está dentro do provider AppService e retorna seu valor na response.

Toda esta estrutura de arquivos deixa o projeto organizado, separando os arquivos por responsabilidade e respeitando os princípios SOLID.

Agora que você entendeu a teoria por trás do Nest, vamos entender o Docker Compose para então podermos “colocar a mão na massa”.

Entendendo o uso de containers com o Docker

O Docker possui uma CLI que permite toda a criação e configuração de containers que podemos precisar usar. Porém, para automatizar processos, é possível criar e executar um arquivo chamado docker-compose.yml. Ele nada mais é do que uma lista de instruções para a orquestração de containers, volumes, redes entre outras coisas que o Docker é capaz de gerenciar. Este arquivo exclui a necessidade de executar comando por comando num terminal, facilitando fazer a configuração em diferentes ambientes.

Um conceito importante para se ter em mente, é o de imagem. Ela é um conjunto de instruções para a criação de um container. Este, por sua vez, é uma instancia de uma imagem.

O que vamos fazer é criar um container que representa uma instância da imagem oficial do PostgreSQL para o Docker, e disponibiliza a porta 5432 para acessar o banco de dados criado nele. Segue o arquivo docker-compose-yml que você deve criar na pasta raiz do projeto:


version: '3' # versão do yaml

services: # representam os containers
  artigo-nestjs-docker-postgres-database: # nome do serviço
    image: postgres # imagem que o docker irá se basear para criar o container
    environment: # variáveis de ambiente
      POSTGRES_PASSWORD: 'postgres'
    container_name: artigo-nestjs-docker-postgres-database # nome do container

    ports: # portas para acessar o container
      - '5432:5432'
    volumes: # onde serão armazenadas as informações do banco
      - .docker/dbdata:/var/lib/postgresql/data

 

Para executá-lo, com o objetivo de criar o container com o banco de dados, é preciso primeiro instalar o Docker for desktop na sua máquina.

Após ter instalado, execute o seguinte comando em um terminal aberto na pasta raiz onde está o seu arquivo:

docker compose up -d

Em caso de sucesso, você terá um container executando um ambiente com um banco de dados postgres, conforme as instruções do arquivo Yaml, sem nenhuma tabela criada nele:

Desenvolvendo nossa própria API

O que temos até o momento é uma aplicação Nest que retorna um clássico “Hello World!” e um ambiente preparado para receber nosso banco de dados.

Com todos os conceitos em mente, é hora de colocá-los em prática e construirmos nossa API a partir do projeto criado pela CLI do Nest. Ela representará uma API de uma biblioteca fictícia, cuja função é armazenar e retornar os livros em estoque.

A tabela de livros armazenará o nome, o endereço de uma imagem ilustrativa do livro (podendo ser um valor nulo) e um identificador único. Além disso, vamos armazenar um valor inteiro que representa a quantidades deste livro em estoque para não complexificar demais este exemplo.

Para lidar com o banco de dados, vamos utilizar uma biblioteca chamada Prisma, um ORM (Object Relational Mapper) para Node.js/TypeScript que facilita nossas consultas ao banco e no gerenciamento de tabelas. Para mais informações sobre esta ferramenta, acesse este artigo.

Vamos lá!

1- Instale o Prisma como dependência na aplicação através do terminal:

npm install prisma @prisma/client

2- Inicialize o Prisma, criando o schema através do terminal:

npx prisma init

O arquivo prisma/schema.prisma permite estruturar o banco de dados e executar o processo de migrate para criar de fato esta estrutura no banco. 

Também foi criado o arquivo .env que armazena as variáveis de ambiente da aplicação. Dentre elas, existe DATABASE_URL com o URL do banco de dados que o Prisma vai acessar.

3- Crie a estrutura das tabelas no arquivo prisma/schema.prisma. Segue o exemplo:

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model Book {
  id       String  @id @default(uuid())
  name     String  @unique
  imageUrl String?
  amount   Int

  @@map("books")
}

4- Acesse o arquivo .env e altere a variável DATABASE_URL de acordo com as credenciais definidas no docker-compose.yml:

DATABASE_URL="postgresql://postgres:[email protected]:5432/postgres?schema=public"

5- Rode o comando de migrate para criar as tabelas no banco de dados:

npx prisma migrate dev

Nomeie a migrate em questão. Darei o nome de init.

6- Para ver o banco de dados facilmente, você pode abrir o Prisma Studio e ver a estrutura das tabelas além dos dados nelas:

npx prisma studio

Acesse http://localhost:5555 no navegador e poderá ver as tabelas:

7- Temos tudo pronto para começar. Primeiro, crie o módulo do Prisma para o Nest. Crie os arquivos:

src/modules/prisma/prisma.service.ts

import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common'
import { PrismaClient } from '@prisma/client'

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
  async onModuleInit() {
    await this.$connect()
    console.log('Database connection has been established.')
  }

  async onModuleDestroy() {
    await this.$disconnect()
  }
}

src/modules/prisma/prisma.module.ts

import { Module, Global } from '@nestjs/common'

import { PrismaService } from './prisma.service'

@Global()
@Module({
  providers: [PrismaService],
  exports: [PrismaService]
})
export class PrismaModule {}

src/modules/prisma/index.ts

export * from './prisma.module'
export * from './prisma.service'
export * from '@prisma/client'

8- Importe este modulo no AppModule:

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { PrismaModule } from './modules/prisma';

@Module({
  imports: [PrismaModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Agora temos um modulo global do Prisma, ou seja, poderemos utilizar o PrismaClient em qualquer classe sem precisar criar uma instância nem mesmo importá-lo em seu respectivo módulo.

9- Crie o BookRepository, que será responsável por abstrair toda a lógica de manipulação do banco de dados. Para isto crie os arquivos:

src/modules/books/repositories/books.repositories.ts

import { Prisma } from '.prisma/client';
import { Injectable } from '@nestjs/common';
import { PrismaService } from 'src/modules/prisma';

@Injectable()
export class BookRepository {
  constructor(private readonly prismaService: PrismaService) {}

  findAll() {
    return this.prismaService.book.findMany();
  }
  findByUnique(input: Prisma.BookWhereUniqueInput) {
    return this.prismaService.book.findUnique({
      where: input,
    });
  }

  create(input: Prisma.BookCreateInput) {
    return this.prismaService.book.create({
      data: input,
    });
  }

  update(input: Prisma.BookUpdateInput, id: string) {
    return this.prismaService.book.update({
      data: input,
      where: {
        id,
      },
    });
  }
  delete(id: string) {
    return this.prismaService.book.delete({
      where: {
        id,
      },
    });
  }
}

src/modules/books/repositories/index.ts

export * from './books.repository';

10- Vamos criar a rota POST /books para poder fazer a adição de um livro no banco de dados. Instale a dependência yup  para lidar com as validações dos inputs das requisições.

npm i yup

Crie os arquivos:

src/modules/books/books.controller.ts

import {
  BadRequestException,
  Body,
  Controller,
  Get,
  Post,
} from '@nestjs/common';
import { yupCreateBookInput } from 'src/yup/books';
import { BookService } from './books.service';
import { CreateBookInput } from './dto/book';

@Controller()
export class BookController {
  constructor(private readonly bookService: BookService) {}

  @Post('/books')
  async createBook(@Body() input: CreateBookInput) {
    // Utiliza um schema yup para verificar o input
    const isValidInput = yupCreateBookInput.isValidSync(input);

    // Caso seja inválido, retorna erro 400
    if (!isValidInput) throw new BadRequestException('Seu input está inválido');

    return this.bookService.createBook(input);
  }
}

src/modules/books/books.service.ts

import {
  ConflictException,
  Injectable,
  InternalServerErrorException,
} from '@nestjs/common';
import { Book } from 'src/models';
import { CreateBookInput } from './dto/book';
import { BookRepository } from './repositories';

@Injectable()
export class BookService {
  constructor(private readonly bookRepository: BookRepository) {}

  async createBook(input: CreateBookInput): Promise {
    // Busca no banco de dados algum livro com o mesmo nome
    const foundBookByName = await this.bookRepository.findByUnique({
      name: input.name,
    });

    // Case exista, retorna erro 409
    if (foundBookByName)
      throw new ConflictException('Já existe um livro com este nome');

    try {
      // Retorna o livro criado
      return this.bookRepository.create(input);
    } catch {
      throw new InternalServerErrorException();
    }
  }
}

src/modules/books/dto/book.ts

export class CreateBookInput {
  name: string;
  imageUrl?: string;
  amount: number;
}

src/modules/books/dto/book.ts

export class CreateBookInput {
  name: string;
  imageUrl?: string;
  amount: number;
}

src/yup/books.ts

import * as yup from 'yup';

export const yupCreateBookInput = yup.object().shape({
  name: yup.string().required(),
  amount: yup.number().required().positive().integer(),
  imageUrl: yup.string().url(),
});

Agora, reúna tudo no BookModule e importe-o no AppModule.

src/modules/books/books.module.ts

import { Module } from '@nestjs/common';
import { BookController } from './books.controller';
import { BookService } from './books.service';
import { BookRepository } from './repositories';

@Module({
  controllers: [BookController],
  providers: [BookRepository, BookService],
})
export class BookModule {}

src/app.module.ts

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { PrismaModule } from './modules/prisma';
import { BookModule } from './modules/books/books.module';

@Module({
  imports: [PrismaModule, BookModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Agora, você já consegue testar a aplicação fazendo a requisição POST /books:

{
  "name": "Exemplo 1",
  "amount": 1
}

11- Vamos criar a rota GET /books para buscar todos os livros cadastrados.

src/modules/books/books.service.ts

// ...

@Injectable()
export class BookService {
  constructor(private readonly bookRepository: BookRepository) {}

  async getBooks(): Promise {
    try {
      // Retorna todos os livros
      return this.bookRepository.findAll();
    } catch {
      throw new InternalServerErrorException();
    }
  }

  // ...
}

src/modules/books/books.controller.ts

// ...
@Controller()
export class BookController {
  constructor(private readonly bookService: BookService) {}

  @Get('/books')
  async getBooks() {
    // Busca e retorna todos os livros cadastrados
    return this.bookService.getBooks();
  }

  // ...
}

Exemplo de retorno da chamada:

[
  {
    "id": "a82eef18-ebea-460d-b38b-d9d309a80739",
    "name": "Exemplo 1",
    "imageUrl": null,
    "amount": 1
  }
]

12- Por último, vamos criar a rota PUT /books/amount para buscar todos os livros cadastrados.

src/modules/books/dto/books.ts

export class CreateBookInput {
  name: string;
  imageUrl?: string;
  amount: number;
}
export class UpdateBookAmountInput {
  amount: number;
}

src/modules/books/books.service.ts

import {
  ConflictException,
  Injectable,
  InternalServerErrorException,
  NotFoundException,
} from '@nestjs/common';
import { Book } from 'src/models';
import { CreateBookInput, UpdateBookAmountInput } from './dto/book';
import { BookRepository } from './repositories';

@Injectable()
export class BookService {
  constructor(private readonly bookRepository: BookRepository) {}
  // ...
  async updateBookAmount(newAmount: number, id: string): Promise {
    // Busca no banco de dados o livro pelo id
    const foundBookById = await this.bookRepository.findByUnique({
      id,
    });

    // Case não exista, retorna erro 404
    if (!foundBookById)
      throw new NotFoundException('Livro não encontrado pelo id');

    try {
      // Retorna o livro atualizado
      return this.bookRepository.update({ amount: newAmount }, id);
    } catch {
      throw new InternalServerErrorException();
    }
  }
}

src/modules/books/books.controller.ts

import {
  BadRequestException,
  Body,
  Controller,
  Get,
  Param,
  Post,
  Put,
} from '@nestjs/common';
import { yupCreateBookInput, yupUpdateBookAmountInput } from 'src/yup/books';
import { BookService } from './books.service';
import { CreateBookInput, UpdateBookAmountInput } from './dto/book';

@Controller()
export class BookController {
  constructor(private readonly bookService: BookService) {}
  // ...
  @Put('/books/amount/:id')
  async updateBookAmount(
    @Body() input: UpdateBookAmountInput,
    @Param() params,
  ) {
    // Utiliza um schema yup para verificar o input
    const isValidInput = yupUpdateBookAmountInput.isValidSync(input);

    // Caso seja inválido, retorna erro 400
    if (!isValidInput)
      throw new BadRequestException('O campo amount é requerido');

    return this.bookService.updateBookAmount(input.amount, params.id);
  }
}

Exemplo do corpo da chamada PUT /books/amount/a82eef18-ebea-460d-b38b-d9d309a80739:

{
  "amount": 2
}

Retorno:

{
  "id": "a82eef18-ebea-460d-b38b-d9d309a80739",
  "name": "Exemplo 1",
  "imageUrl": null,
  "amount": 2
}

Concluindo

Após a leitura deste artigo, você é capaz de criar uma API RESTful em NestJS que manipula informações de um banco de dados PostgreSQL — que é executado em um container através do Docker — e tem uma estrutura de arquivos ideal para seguir os princípios SOLID da programação orientada a objetos.

 


Quer escrever na Prensa?

Junte-se a uma comunidade de Creators que estão melhorando a internet com artigos inteligentes, relevantes e humanos. Além disso, seu artigo pode fazer parte do Projeto de Monetização, e você pode ganhar dinheiro com ele!

Clique aqui para se cadastrar e venha com a gente!


Topo