Criando um CRUD no Nest.JS com o Prisma

Este post integra uma série dedicada à criação da aplicação RPS WEB (XML). Você pode acompanhar todos os posts dessa série utilizando a tag #rpsweb.

O NestJS é um framework moderno e progressivo para construção de aplicativos eficientes e escaláveis em Node.js. Com uma arquitetura modular e orientada a objetos, o NestJS combina elementos familiares do Angular, TypeScript e Node.js para oferecer uma experiência de desenvolvimento consistente e poderosa.

Neste artigo, exploraremos os principais recursos do NestJS, destacando sua sintaxe limpa, organização intuitiva de código e suporte para testes automatizados. Se você está buscando uma solução moderna e eficiente para o desenvolvimento de APIs em Node.js, o NestJS é uma escolha promissora que vale a pena explorar.

Repositório

API REST (Nest.JS + MySQL)
https://github.com/ramonwiller/rpsweb-api

Commit: https://github.com/ramonwiller/rpsweb-api/tree/600508f032ccb0cd79565c9270ffebe751814fae

Pré-requisitos

  • Certifique-se de que o Node.js (versão >= 20) esteja instalado em seu sistema operacional.
  • Para o armazenamento dos dados será utilizado o banco de dados MySQL (versão >= 8).

Configuração do Ambiente

Não considero uma boa prática instalar todas as dependências do projeto logo no início. Existe o risco de incluir uma dependência que não será utilizada, o que pode resultar em maior complexidade e potencialmente expor o projeto a vulnerabilidades de segurança desnecessárias. No entanto, para fins didáticos deste artigo, irei abrir uma exceção a essa regra e instalar todas as dependências necessárias de uma vez.

Nest CLI é uma ferramenta de interface de linha de comando que ajuda você a inicializar, desenvolver e manter seus aplicativos Nest.

npm i -g @nestjs/cli

Com o Nest CLI instalado, vamos criar um novo projeto utilizando o npm como gerenciador de pacotes. No entanto, sinta-se à vontade para escolher outro gerenciador, caso prefira.

nest new rpsweb


Após criar o projeto, navegue até o diretório “rpsweb” e utilize os comandos abaixo para instalar as dependências necessárias para produção e desenvolvimento:

cd rpsweb

# Dependências de Produção
npm i --save @prisma/client class-validator class-transformer bcrypt

# Dependências de Desenvolvimento
npm i --save-dev prisma @types/bcrypt

Após a instalação de todas as dependências necessárias, proceda com a configuração do Prisma no seu projeto executando o comando a seguir.

npx prisma init

Este comando criará um arquivo chamado .env na raiz do projeto para configurar as variáveis de ambiente necessárias. Além disso, também criará o arquivo prisma/schema.prisma, onde você definirá os modelos utilizados no projeto. Esses modelos serão refletidos no banco de dados na forma de tabelas.

Caso não deseje utilizar o MySQL, você tem a opção de escolher entre outros bancos de dados, como PostgreSQL, SQLite, SQL Server, MongoDB e CockroachDB. Você pode consultar as strings de conexão para esses bancos de dados em https://pris.ly/d/connection-strings. Supondo que você opte pelo MySQL, você precisará alterar a propriedade provider no arquivo prisma/schema.prisma, conforme definido abaixo.

# prisma/schema.prisma
datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

Ao alterar a propriedade provider, é necessário também ajustar a variável de ambiente DATABASE_URL no arquivo .env de acordo com a string de conexão do MySQL, conforme mostrado abaixo:

DATABASE_URL="mysql://root:password@localhost:3306/rpsweb"

Criação do Banco de Dados

A gestão do banco de dados em uma aplicação NestJS utilizando o Prisma é realizada através do arquivo prisma/schema.prisma. Esse arquivo contém as definições das tabelas e dos relacionamentos do projeto. No contexto do Prisma, a relação entre o modelo e a tabela no banco de dados é estabelecida através do decorador @@map("nome_da_tabela"), onde ‘nome_da_tabela’ indica o nome da tabela no banco de dados. Se não for realizado esse mapeamento, por padrão, a tabela terá o mesmo nome do modelo, por exemplo, no caso do modelo ‘User’, a tabela também será chamada ‘User’.

# prisma/schema.prisma
datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

model User {
  id        Int       @id @default(autoincrement())
  name      String    @db.VarChar(100)
  email     String    @unique @db.VarChar(255)
  password  String    @db.VarChar(255)
  createdAt DateTime  @default(now())
  updatedAt DateTime? @updatedAt

  @@map("users")
}

Após definir o model User, e necessário criar uma migração utilizando o comando a seguir, este comando criara o arquivo prisma\migrations\[timestamp]_create_table_users\migration.sql

npx prisma migrate dev --name create_table_users

Ao criar uma migração, o Prisma aplicará automaticamente as alterações no banco de dados. No entanto, se por algum motivo desejar aplicar essas mudanças manualmente, você pode utilizar o seguinte comando:

npx prisma migrate deploy

Neste artigo, evitaremos entrar em detalhes sobre a arquitetura do NestJS. Por enquanto, é importante entender que o NestJS organiza o código em módulos, que encapsulam controllers, services e outros recursos. Além disso, o NestJS se destaca pela sua forte adoção do princípio da injeção de dependências. Essa abordagem permite uma organização mais eficiente do código, desacoplando os componentes, facilitando a reutilização do código, melhorando a testabilidade e contribuindo para uma manutenção mais fácil e uma legibilidade aprimorada.

Para integrar o Prisma em nosso projeto, é necessário criar um módulo dedicado e um serviço correspondente. Este serviço será então injetado em outros serviços da nossa aplicação, possibilitando as interações com o banco de dados de forma centralizada e eficiente. O comando a seguir criará os respectivos arquivos necessários para essa integração.

nest g module prisma
# CREATE src/prisma/prisma.module.ts (87 bytes)
# UPDATE src/app.module.ts (260 bytes)
nest g service prisma
# CREATE src/prisma/prisma.service.ts (94 bytes)
# CREATE src/prisma/prisma.service.spec.ts (478 bytes)
# UPDATE src/prisma/prisma.module.ts (167 bytes)

Substitua o conteúdo do arquivo src/prisma/prisma.service.ts pelo seguinte conteúdo:

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

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
  async onModuleInit() {
    await this.$connect();
  }
}

Para permitir o uso de um serviço através da injeção de dependência em outro módulo, é necessário exportá-lo. Isso é feito definindo a propriedade ‘exports’ no decorator do módulo, como exemplificado abaixo, onde exportaremos o serviço do PrismaService:

# src\prisma\prisma.module.ts
import { Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';

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

Remova o PrismaModule do arquivo src\app.module.ts. Ele será importado apenas nos serviços que utilizarem recursos do banco de dados. Além disso, remova os arquivos desnecessários.:

src\app.controller.spec.ts
src\app.controller.ts
src\app.service.ts

O arquivo deverá parecer com o listado abaixo:

# src\app.module.ts
import { Module } from '@nestjs/common';

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

Resource User

A CLI (Interface de Linha de Comando) do NestJS proporciona uma funcionalidade de geração (generate) que simplifica a criação dos pontos de entrada (entry points) necessários para operações básicas de CRUD (Create, Read, Update, Delete). Para fazer uso desse recurso, podemos empregar o comando a seguir:

nest generate resource user

Após executar o comando mencionado acima, você será solicitado a selecionar a camada de transporte dos dados. Em nosso caso, optaremos por utilizar o REST API, o que pode ser confirmado pressionando a tecla Enter. Em seguida, para prosseguir com a geração dos pontos de entrada (entry points), basta confirmar digitando ‘Y’ ou pressionar Enter novamente.

Todos os arquivos necessários para o nosso CRUD foram gerados com sucesso e estão localizados dentro da pasta /src/user, conforme indicado na saída do comando.

Remova o arquivo src/user/entities/user.entity.ts e a pasta correspondente src/user/entities, pois optaremos por utilizar o modelo User fornecido pelo Prisma.

Faça uma pequena alteração no arquivo src\user\user.controller.ts, substituindo @Controller(‘user’) por @Controller(‘users’), conforme indicado:

# src\user\user.controller.ts
# imports ...
@Controller('users')
export class UserController {
  constructor(private readonly userService: UserService) {}
  # endpoints ...
}

Agora vamos importar o PrismaModule em nosso UserModule. Para isso, faça as seguintes alterações no arquivo src\user\user.module.ts, conforme demonstrado abaixo:

# src\user\user.module.ts
# imports ...
import { PrismaModule } from 'src/prisma/prisma.module';

@Module({
  controllers: [UserController],
  providers: [UserService],
  imports: [PrismaModule],
})
export class UserModule {}

Após as alterações no UserController e UserModule, é hora de criar os DTOs (Objetos de Transferência de Dados) para validar e mapear os dados do usuário. Esses DTOs definem quais dados serão transferidos entre os controllers e os services, contribuindo para uma melhor coesão, organização e manutenção do código. Um exemplo disso é o DTO CreateUserDto, que será elaborado conforme demonstrado abaixo:

import { IsEmail,IsNotEmpty, IsString, Length, MinLength } from 'class-validator';

export class CreateUserDto {
  @Length(3, 100)
  @IsNotEmpty()
  @IsString()
  name: string;

  @IsEmail()
  @IsNotEmpty()
  @IsString()
  email: string;

  @MinLength(8)
  @IsNotEmpty()
  @IsString()
  password: string;
}

Para evitar a repetição de código, o DTO para atualização de um usuário, chamado UpdateUserDto, estende o DTO de criação, CreateUserDto, aproveitando sua estrutura. Essa abordagem permite realizar alterações parciais em um usuário, como modificar apenas o nome, por exemplo. Para isso, é utilizada a função PartialType, pacote @nestjs/mapped-types, conforme demonstrado abaixo:

import { PartialType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto';

export class UpdateUserDto extends PartialType(CreateUserDto) {}

Agora, avançaremos para a criação do serviço, que abriga toda a lógica de negócios da nossa aplicação. Para interagir com o banco de dados, vamos injetar o PrismaService em nosso UserService por meio do construtor da classe. Além disso, observe que estamos utilizando o bcrypt para criptografar a senha fornecida pelo usuário. Para evitar a repetição de código e centralizar a lógica em um único ponto, criamos uma função privada chamada cryptPassword. Essa função recebe a senha fornecida pelo usuário e a devolve criptografada.

import { Injectable } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { PrismaService } from 'src/prisma/prisma.service';
import * as bcrypt from 'bcrypt';

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

  async create(createUserDto: CreateUserDto) {
    createUserDto.password = await this.cryptPassword(createUserDto.password);
    return await this.prismaService.user.create({ data: createUserDto });
  }

  findAll() {
    return this.prismaService.user.findMany();
  }

  findOne(id: number) {
    return this.prismaService.user.findUnique({ where: { id } });
  }

  async update(id: number, updateUserDto: UpdateUserDto) {
    if (updateUserDto.password) {
      updateUserDto.password = await this.cryptPassword(updateUserDto.password);
    }

    return this.prismaService.user.update({
      data: updateUserDto,
      where: { id },
    });
  }

  remove(id: number) {
    return this.prismaService.user.delete({ where: { id } });
  }

  private async cryptPassword(password: string): Promise<string> {
    const salt = await bcrypt.genSalt();
    return bcrypt.hash(password, salt);
  }
}

Se você chegou até aqui, alcançou o objetivo central deste artigo: a implementação das quatro operações essenciais – criar, editar, excluir e recuperar um usuário. Agora, é hora de testar essas funcionalidades iniciando o servidor fornecido com o Node. No entanto, ao recuperar os dados do usuário, você pode notar que a senha do usuário, mesmo criptografada, é retornada. Isso representa uma falha de segurança potencial.

Para resolver esse problema, uma abordagem seria criar um método que recebe o usuário como parâmetro e retorna uma versão dele sem a propriedade “password”. Embora outra alternativa seja a serialização do objeto, neste artigo optarei por demonstrar o uso de um UserInterceptor, uma ferramenta útil oferecida pelo framework NestJS. No entanto, em projetos mais amplos, é aconselhável considerar a serialização como uma opção mais robusta.

import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable()
export class UserInterceptor implements NestInterceptor {
  removePassword(obj: any): any {
    if (Array.isArray(obj)) {
      return obj.map((item) => this.removePassword(item));
    } else if (obj !== null && typeof obj === 'object') {
      for (const key in obj) {
        if (key === 'password') {
          delete obj[key];
        } else {
          obj[key] = this.removePassword(obj[key]);
        }
      }
    }
    return obj;
  }

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      map((data) => {
        return this.removePassword(data);
      }),
    );
  }
}

Não se esqueça de registrar o UserInterceptor no arquivo src\main.ts. Além disso, é crucial incluir o ValidationPipe para garantir a validação adequada dos dados de entrada. No final, seu arquivo deve se assemelhar ao exemplo abaixo:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
import { UserInterceptor } from './user/user.interceptor';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.useGlobalInterceptors(new UserInterceptor());

  app.useGlobalPipes(
    new ValidationPipe({
      transform: true,
      whitelist: true,
      forbidNonWhitelisted: true,
    }),
  );

  await app.listen(3000);
}
bootstrap();

Agora é o momento de colocarmos a aplicação em funcionamento. A partir do diretório da aplicação, você poderá executar o seguinte comando. No entanto, antes de prosseguir, certifique-se de ter configurado corretamente o acesso ao banco de dados e de ter realizado o deploy das migrações necessárias. Abaixo estão os comandos que você utilizará para realizar essas etapas importantes.

# Deploy Banco de dados
npx prisma migrate deploy

# Executar a aplicação
npm run start:dev 

O Prisma oferece uma interface web que pode auxiliar na visualização dos dados do seu banco. Para acessá-la, você pode executar o comando a seguir após a inicialização da sua aplicação.

# Prisma Studio
npx prisma studio

Após seguir os passos acima, é hora de validar o funcionamento da aplicação realizando algumas requisições. Sinta-se à vontade para executar as requisições que julgar necessárias para garantir o correto funcionamento do sistema.

# create
curl --request POST \
  --url http://localhost:3000/users \
  --header 'Content-Type: application/json' \
  --data '{
	"name": "Ramon Willer",
	"email": "willer.ramon@gmail.com",
	"password": "12345678"
}'

# findAll
curl --request GET \
  --url http://localhost:3000/users

# findOne
curl --request GET \
  --url http://localhost:3000/users/1

# update
curl --request PATCH \
  --url http://localhost:3000/users/1 \
  --header 'Content-Type: application/json' \
  --data '{
	"name": "Ramon Willer"
}'

# delete
curl --request DELETE \
  --url http://localhost:3000/users/1

Agradeço por acompanhar este artigo e ficarei feliz em receber qualquer feedback ou sugestão para aprimorar minha experiência no desenvolvimento de software. Se houver algo que você gostaria de destacar ou algum ponto que mereça mais atenção, por favor, compartilhe.