DEV Community

Cover image for [PT-BR] CRUD com NestJS e MongoDB
Leonardo Santos
Leonardo Santos

Posted on

[PT-BR] CRUD com NestJS e MongoDB

Uma das tecnologias que curto bastante estudar em meu tempo livre é Node. E, recentemente eu comecei a estudar um dos frameworks para se trabalhar com Node: NestJS.

Por já ter trabalhado com alguns frameworks tanto no frontend quanto no backend, o Nest me lembrou bastante conceitos que usamos quando trabalhamos com Angular e Spring Boot, como por exemplo Módulos, Filtros e Decorators.

Por já ter estudado um pouco desse framework e gostado bastante, gostaria de explicar aqui como seria possível fazer uma API para um CRUD básico. Dessa forma, se você estiver querendo se familiarizar com a ferramenta ou como trabalhar com Node, você pode ter mais uma referência.

Em relação ao banco de dados, irei utilizar o MongoDB.

A referência que utilizarei para esse artigo será a documentação do Nest e a documentação do Mongoose.

Vamos lá!

Mão na massa

Pré requisitos

Para começarmos a instalar o Nest e desenvolvermos a nossa aplicação, é necessário que tenhamos o Node instalado na nossa máquina. Para instalá-lo, podemos acessar esse link.

Para o desenvolvimento desse artigo eu estou utilizando o Node na versão 12.18.4 LTS.

Instalando CLI

Primeiro, vamos instalar o CLI do NestJS na nossa máquina. Em algum terminal de linha de comando, execute o seguinte comando:

## No Linux ou Mac
$ sudo npm install -g @nestjs/cli

## No Windows
$ npm install -g @nestjs/cli
Enter fullscreen mode Exit fullscreen mode

Criando projeto

Já com o CLI do Nest instalado na nossa máquina, temos que ir até o diretório que desejamos criar o projeto e executar o seguinte comando:

$ nest new <nome-do-projeto>
Enter fullscreen mode Exit fullscreen mode

Substitua o <nome-do-projeto> para o nome que desejar.

O próprio CLI irá nos perguntar se nós desejamos instalar as dependências do nosso projeto com o NPM ou com Yarn. Fique a vontade para escolher qualquer um dos dois.

Executando o projeto

Após ter criado o projeto e entrado na pasta do projeto, via algum terminal de linha de comando, execute o seguinte comando para executar o projeto:

$ npm start
Enter fullscreen mode Exit fullscreen mode

Se desejar executar o projeto em modo watch, execute o seguinte comando:

$ npm run start:dev
Enter fullscreen mode Exit fullscreen mode

Configurando a conexão ao banco de dados

Fique à vontade para escolher como desejará criar o banco de dados: seja ele na nossa própria máquina ou em algum lugar remoto. A nossa aplicação precisará apenas da string de conexão ao banco de dados.

Instalando dependências para trabalhar com MongoDB

Para trabalharmos com MongoDB em um projeto com Nest, existem algumas bibliotecas obrigatórias que precisaremos instalar no nosso projeto:

## Para trabalharmos com MongoDB em um projeto Nest
$ npm install mongoose @nestjs/mongoose

## Para termos o suporte do Typescript ao trabalharmos com o Mongoose
$ npm install -D @types/mongoose
Enter fullscreen mode Exit fullscreen mode

Configurando a string de conexão

Com as bibliotecas instaladas, precisaremos configurar nossa aplicação para que ela possa se conectar ao MongoDB. Faremos isso ao abrir o arquivo src/app.module.ts e incluirmos o módulo do Mongoose, definindo a nossa string de conexão do banco:

import { Module } from '@nestjs/commons';

// Importamos o módulo do Mongoose
import { MongooseModule } from '@nestjs/mongoose';

@Module({
  // Suponhamos aqui que a nossa string de conexão seja 'mongodb://localhost/nest'
  imports: [MongooseModule.forRoot('mongodb://localhost/nest')]
})

export class AppModule()
Enter fullscreen mode Exit fullscreen mode

Dica

Se precisarmos salvar esse código em algum repositório público, como Github, Gitlab e outros, sugiro a utilização da biblioteca dotenv. Com ela, poderemos definir variáveis de ambiente para o nosso projeto e, por conta disso, poderemos salvar nossa string de conexão ao banco como uma variável de ambiente. Assim não precisaremos expor esse e algum outro dado sensível da nossa aplicação.

Ao fazer isso, teoricamente, deveremos ter acesso ao banco de dados através da nossa aplicação.

Tente executar a aplicação nesse momento. Se houver algum problema ao tentar se conectar ao banco, surgirá uma mensagem em vermelha no seu terminal (onde tivermos executado o comando para executar a aplicação) dizendo que não foi possível se conectar ao banco de dados:
Erro ao tentar se conectar ao banco de dados

Nosso Model

Para quem não trabalhou com o Mongoose ainda, entenda que, basicamente, tudo que ele trabalha é derivado de um Schema. Os Schemas que ele trabalha irão mapear as nossas classes para um formato de Coleção e de seus respectivos Documentos no MongoDB.

De uma forma geral, eu gosto de interpretar que os Schemas, no Mongoose, fazem analogia à forma que trabalhamos com as Models em outros ORMs tradicionais para banco de dados relacionais.

Utilizando do sugestivo tema do Nest e, também, para exemplificar melhor o entendimento da construção da nossa API, vamos trabalhar com Gato.

Antes de criarmos nosso Schema, vamos organizar o nosso contexto de API em um módulo. Ou seja, a API específica para Gato que iremos criar estará toda organizada em um módulo. E, para isso, vamos executar o seguinte comando no nosso terminal:

$ nest generate module gatos
Enter fullscreen mode Exit fullscreen mode

Após a execução desse comando, repare que foi criado um subdiretório chamado gatos dentro do diretório src do nosso projeto. Dentro dele irá conter o arquivo gatos.module.ts. Repare também que o nosso GatoModule (nome do módulo que foi criado) já foi importado no nosso arquivo AppModule.

Criando o Schema Gato

Agora que já criamos o nosso módulo, vamos criar nosso Schema. Vamos gerar a nossa classe através do CLI do Nest - igual fizemos com o nosso GatoModule - e vamos transformá-la em um Schema.

Para criar a nossa classe Gato, vamos executar o seguinte comando:

$ nest generate class gatos/gato
Enter fullscreen mode Exit fullscreen mode

Repare que arquivo gato.ts foi criado dentro do subdiretório gato e seu respectivo arquivo de teste também foi criado, o gato.spec.ts.

O conteúdo do arquivo gato.ts, no momento, é basicamente o export da classe. Para transformarmos ela em um Schema para que o Mongoose consiga mapeá-la no MongoDB, precisaremos fazer o seguinte: estender a classe Document do Mongoose. E, também, precisamos adicionar o Decorator @Schema() em cima da declação da nossa classe Gato.

As modificações deixarão o arquivo com o seguinte conteúdo:

import { Schema } from '@nestjs/mongoose';
import { Document } from 'mongoose';

@Schema()
export class Gato extends Document {}
Enter fullscreen mode Exit fullscreen mode

Vamos adicionar algumas propriedades à nossa classe Gato, utilizando o Decorator @Prop() da biblioteca @nestjs/mongoose:

import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';

@Schema()
export class Gato extends Document {

  @Prop()
  nome: string;

  @Prop()
  idade: number;

  @Prop()
  raca: string;
}
Enter fullscreen mode Exit fullscreen mode

O Decorator @Schema(), por si só, não irá criar de fato um Schema. O papel dele é apenas marcar a nossa classe como algo que poderá ser mapeado, no banco de dados, como uma Coleção. É válido dizer que da forma que definimos ele, sem passar nenhum parâmetro, ele irá mapear essa classe como uma Coleção e irá adicionar o s no final do nome da Coleção no banco de dados. Ou seja, ele irá entender que essa classe está relacionada à coleção Gatos no banco de dados.

Agora, para criarmos de fato o Schema baseado nessa classe e exportá-lo, precisamos adicionar a seguinte instrução no final do nosso arquivo gato.ts:

export const CatSchema = SchemaFactory.createForClass(Gato);
Enter fullscreen mode Exit fullscreen mode

A classe SchemaFactory deve ser importada da biblioteca @nestjs/mongoose. Nosso arquivo final ficará da seguinte forma:

import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';

@Schema()
export class Gato extends Document {

  @Prop()
  nome: string;

  @Prop()
  idade: number;

  @Prop()
  raca: string;
}

export const GatoSchema = SchemaFactory.createForClass(Gato);
Enter fullscreen mode Exit fullscreen mode

Registrando o Schema Gato

Agora que criamos a nossa classe e o nosso schema, precisamos registrá-los no nosso módulo Gato e no módulo do Mongoose. Dessa forma, o Mongoose entenderá que a nossa classe e o nosso schema estarão definidos nesse escopo somente.

Então, para fazermos esse registro, vamos definir o conteúdo do nosso GatoModule dessa forma:

import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { Gato, GatoSchema } from './gato';

@Module({
  imports: [
    MongooseModule.forFeature([
      {
        name: Gato.name,
        schema: GatoSchema
      }
    ])
  ],
  exports: [],
  controllers: [],
  providers: []
})
export class GatosModule {}

Enter fullscreen mode Exit fullscreen mode

Se caso quisermos utilizar a nossa classe e schema em algum outro módulo, precisamos apenas adicionarmos o MongooseModule dentro do array exports e, no módulo que formos utilizar a classe e/ou o schema, adicionarmos o MongooseModule no array imports.

Criando o Service Gato

A classe responsável por "conversar" com o MongoDB através do Mongoose, será o nosso GatosService. Para criarmos esse Service, precisamos executar o seguinte comando:

$ nest generate service gatos
Enter fullscreen mode Exit fullscreen mode

Ao executar esse comando, dois arquivos serão criados no subdiretório src/gatos: o gatos.service.ts e gatos.service.spec.ts. O comando também irá adicionar o GatosService como um provider no GatosModule.

Para o nosso CRUD, iremos definir os seguintes métodos e suas respectivas implementações:

import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { Gato } from './gato';

@Injectable()
export class GatosService {
  constructor(
    @InjectModel(Gato.name) private gatoModel: Model<Gato>
  ) {}

  async listarTodos(): Promise<Gato[]> {
    return this.gatoModel.find().exec();
  }

  async criar(gato: Gato): Promise<Gato> {
    const gatoCriado = new this.gatoModel(gato);

    return gatoCriado.save();
  }

  async buscarPorId(id: string): Promise<Gato> {
    return this.gatoModel.findById(id).exec();
  }

  async atualizar(id: string, gato: Gato): Promise<Gato> {
    return this.gatoModel.findByIdAndUpdate(id, gato).exec();
  }

  async remover(id: string) {
    const gatoApagado = this.gatoModel.findOneAndDelete({ _id: id }).exec();

    return (await gatoApagado).remove();
  }
}

Enter fullscreen mode Exit fullscreen mode

Sobre o construtor do Service

A biblioteca @nestjs/mongoose nos fornece um meio para que possamos trabalhar com determinado Documento através da injeção de dependências com o Decorator @InjectModel. Para esse Decorator, precisamos apenas passar o nome da classe que foi marcada com o Decorator @Schema().

Sobre os métodos de CRUD

As implementações dos métodos podem variar de acordo com os tratamentos que acreditamos que sejam necessários. Para fins de praticidade, eu os implementei da forma acima. Porém, para um CRUD mais elaborado, é válido que seja aplicado alguns tratamentos de segurança e prevenção de erros (por exemplo: verificar se o objeto existe no banco de dados antes de tentarmos alterá-lo).

Criando o Controller Gato

A classe responsável por receber as requisições HTTP para trabalhar com o nosso CRUD de Gato será o nosso GatosController. Para criarmos esse Service, precisamos executar o seguinte comando:

$ nest generate controller gatos
Enter fullscreen mode Exit fullscreen mode

Ao executar esse comando, o arquivo gatos.controller.ts será criado no subdiretório src/gatos. O comando também irá adicionar o GatosController como um controller no GatosModule.

A implementação do nosso GatosController será a seguinte:

import { Body, Controller, Delete, Get, Param, Post, Put } from '@nestjs/common';
import { Gato } from './gato';
import { GatosService } from './gatos.service';

@Controller('gatos')
export class GatosController {
  constructor(
    private readonly gatosService: GatosService
  ) {}

  @Get()
  async listarTodos(): Promise<Gato[]> {
    return this.gatosService.listarTodos();
  }

  @Post()
  async criar(@Body() gato: Gato): Promise<Gato> {
    return this.gatosService.criar(gato);
  }

  @Get(':id')
  async buscarPorId(@Param('id') id: string): Promise<Gato> {
    return this.gatosService.buscarPorId(id);
  }

  @Put(':id')
  async atualizar(@Param('id') id: string, @Body() gatoAtualizado: Gato): Promise<Gato> {
    return this.gatosService.atualizar(id, gatoAtualizado);
  }

  @Delete(':id')
  async remover(@Param('id') id: string): Promise<Gato> {
    return this.gatosService.remover(id);
  }

}

Enter fullscreen mode Exit fullscreen mode

Sobre o construtor do Controller

Assim como no nosso GatosService, o construtor do GatosController utilizará da injeção de dependência para que possamos acessar os métodos do nosso GatosService.

Sobre os Decorators do Controller

O Nest nos provê diversos Decorators que devemos utilizar em nossos Controllers.

Primeiro, para que possamos marcar uma classe como Controller, precisamos adicionar o Decorator @Controller() em cima da declaração da classe. Como argumento opcional, podemos passar um prefixo de rota para que possamos agrupar as rotas dessa classe em um mesmo caminho. Ou seja, de acordo com a implementação acima, todas as rotas que implementarmos nessa classe terá o prefixo /gatos.

Também temos Decorators para os métodos HTTP das nossas rotas:

  • Para requisições com o método GET precisamos definir o Decorator @Get();
  • Para requisições com o método POST precisamos definir o Decorator @Post();
  • Para requisições com o método PUT precisamos definir o Decorator @Put();
  • Para requisições com o método DELETE precisamos definir o Decorator @Delete().

Cada um desses Decorators de métodos HTTP podem receber um parâmetro que definirão os parâmetros da rota. No nosso exemplo, definimos apenas o parâmetro :id em algumas das nossas rotas.

Quando definimos um parâmetro de rota, podemos ter acesso a ele através do Decorator @Param() como argumento de nosso método. Apenas precisamos passar o nome do parâmetro que queiramos ter acesso. No nosso exemplo, definimos o parâmetro :id.

Para as rotas que esperamos um conteúdo no corpo da nossa requisição, utilizamos o Decorator @Body() como argumento de nosso método. Dessa forma, teremos acesso ao objeto que estará contido no corpo da nossa requisição através do argumento relacionado ao Decorator @Body().

Acessando nossa aplicação

Após configurarmos a conexão ao MongoDB, criarmos nossa Model e Schema, criarmos nosso Service e nosso Controller, já conseguiremos fazer o uso dessa aplicação.

Para acessar as rotas do nosso CRUD, inicie a aplicação e faça as requisições nas rotas que foram criadas no nosso Controller.

Dica

Ao iniciar a aplicação com as rotas criadas, no mesmo terminal que você executou o comando de execução da aplicação serão listadas as rotas que a aplicação já estará esperando por requisições:

Log de rotas no terminal que a aplicação foi executada

Finalizando

A aplicação que foi criada nesse artigo pode ser encontrada nesse link. Em caso de dúvidas, estou sempre aberto a sugestões, críticas e ideias! o/

Top comments (0)