Como Criar um CRUD com NestJS e Prisma: Schema, Relacionamentos e Estruturação de Projeto

Posted by

Criar uma API REST com NestJS e Prisma ORM é uma abordagem moderna e eficiente para o desenvolvimento backend em Node.js. Este artigo apresenta um guia prático e completo para implementar um sistema CRUD, cobrindo desde a definição do schema até a implementação de relacionamentos, validação de dados, tratamento de erros personalizados e uma arquitetura clara que separa responsabilidades entre Controller, Service, Repository e DTOs.

Usaremos como exemplo um sistema com entidades como User, Profile, Order e Product, explorando todos os tipos de relacionamentos (One-to-One, One-to-Many e Many-to-Many) e boas práticas recomendadas pelo ecossistema NestJS e Prisma.

Pré-requisitos para o Projeto

Antes de começar, é essencial dominar os seguintes conceitos:

  1. Schema do Prisma: Definição de modelos e relacionamentos.
  2. Relacionamentos: One-to-One, One-to-Many e Many-to-Many.
  3. DTOs (Data Transfer Objects): Validação e estruturação de dados.
  4. Tratamento de Erros Personalizados: Capturar exceções e retornar mensagens claras ao cliente.
  5. Arquitetura Modular: Separação de camadas (Controller, Service, Repository).

Setup do Projeto: Instalando NestJS e Prisma

Comece criando um novo projeto NestJS:

npm install -g @nestjs/cli
nest new nestjs-prisma-crud

Instale o Prisma como dependência de desenvolvimento:

npm install --save-dev prisma
npx prisma init

Configure a conexão com o MySQL no arquivo .env:

DATABASE_URL="mysql://user:password@localhost:3306/crud_prisma"

Definindo o Schema com Relacionamentos

No arquivo prisma/schema.prisma, defina os modelos com os três tipos de relacionamentos:

One-to-One: User ↔ Profile

model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  profile   Profile?
  @@map("users")
}

model Profile {
  id        Int      @id @default(autoincrement())
  bio       String
  user      User     @relation(fields: [userId], references: [id])
  userId    Int      @unique
  @@map("profiles")
}

One-to-Many: User ↔ Order

model Order {
  id        Int      @id @default(autoincrement())
  total     Float
  userId    Int
  user      User     @relation(fields: [userId], references: [id])
  items     OrderItem[]
  status    String
  createdAt DateTime
  @@map("orders")
}

Many-to-Many: Order ↔ Product

O Prisma não suporta diretamente relacionamentos Many-to-Many, mas cria uma tabela intermediária automaticamente:

model Product {
  id        Int      @id @default(autoincrement())
  name      String
  orders    OrderItem[]
  @@map("products")
}

model OrderItem {
  id        Int      @id @default(autoincrement())
  orderId   Int
  productId Int
  quantity  Int
  product   Product  @relation(fields: [productId], references: [id])
  order     Order    @relation(fields: [orderId], references: [id])
  @@map("order_items")
}

Configuração do Banco com @@map e @@index

Use @@map para personalizar nomes de tabelas e campos no banco de dados:

model Product {
  id   Int   @id @default(autoincrement())
  name String @map("product_name")
  @@map("products")
}

Adicione índices para otimizar consultas frequentes:

model Order {
  id        Int      @id @default(autoincrement())
  status    String
  createdAt DateTime @default(now())
  @@index([status, createdAt])
}

Aplique as migrações:

npx prisma migrate dev --name init

Validação de Dados e DTOs

DTOs garantem que os dados recebidos nas requisições sejam válidos e seguros.

  1. Ative a validação global em main.ts:
// src/main.ts
import { ValidationPipe } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe({ transform: true }));
  await app.listen(3000);
}
  1. Crie DTOs com validação usando class-validator:
// src/orders/dto/create-order.dto.ts
import { IsNumber, IsArray } from 'class-validator';

export class CreateOrderDto {
  @IsNumber()
  total: number;

  @IsArray()
  products: { id: number; quantity: number }[];
}

Estrutura de Pastas e Arquivos Importantes

nestjs-prisma-crud/

├── prisma/
│ ├── schema.prisma # Modelos e relacionamentos
│ └── migrations/ # Migrações geradas pelo Prisma

├── src/
│ ├── app.module.ts # Módulo principal da aplicação
│ ├── main.ts # Entry point com validação global e filtros

│ ├── prisma/
│ │ ├── prisma.service.ts # Serviço de acesso ao Prisma
│ │ └── prisma-error.filter.ts# Filtro de erro personalizado

│ ├── common/ # Utilitários compartilhados
│ │ ├── filters/ # Outros filtros de exceção
│ │ ├── interceptors/ # Interceptadores (ex: logging, transformação)
│ │ ├── decorators/ # Decorators customizados
│ │ └── constants/ # Constantes globais

│ ├── users/
│ │ ├── dto/
│ │ │ ├── create-user.dto.ts
│ │ │ └── update-user.dto.ts
│ │ ├── entities/
│ │ │ └── user.entity.ts
│ │ ├── users.controller.ts
│ │ ├── users.service.ts
│ │ ├── users.repository.ts # (opcional)
│ │ └── users.module.ts

│ ├── profiles/
│ │ ├── dto/
│ │ ├── entities/
│ │ ├── profiles.controller.ts
│ │ ├── profiles.service.ts
│ │ └── profiles.module.ts

│ ├── orders/
│ │ ├── dto/
│ │ │ └── create-order.dto.ts
│ │ ├── entities/
│ │ │ └── order.entity.ts
│ │ ├── orders.controller.ts
│ │ ├── orders.service.ts
│ │ └── orders.module.ts

│ ├── products/
│ │ ├── dto/
│ │ ├── entities/
│ │ ├── products.controller.ts
│ │ ├── products.service.ts
│ │ └── products.module.ts

│ ├── order-items/ # Entidade pivot do Many-to-Many
│ │ ├── dto/
│ │ ├── entities/
│ │ ├── order-items.controller.ts
│ │ ├── order-items.service.ts
│ │ └── order-items.module.ts

├── test/ # Testes unitários e e2e
│ ├── app.e2e-spec.ts
│ └── jest-e2e.json

├── .env # Variáveis de ambiente (ex: DATABASE_URL)
├── .gitignore
├── package.json
├── tsconfig.json
└── README.md

PrismaService

Gerencia a conexão com o banco e implementa hooks do ciclo de vida da aplicação:

// src/prisma/prisma.service.ts
import {
  Injectable,
  OnModuleDestroy,
  OnApplicationBootstrap,
} from '@nestjs/common';
import { PrismaClient } from 'generated/prisma';

@Injectable()
export class PrismaService
  extends PrismaClient
  implements OnApplicationBootstrap, OnModuleDestroy
{
  async onApplicationBootstrap() {
    await this.$connect();
  }

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

Rode o comando para gerar o PrismaClient:

npx prisma generate

AppModule

Registre o PrismaService no módulo principal:

// src/app.module.ts
import { Module } from '@nestjs/common';
import { PrismaService } from './prisma/prisma.service';
import { OrdersModule } from './orders/orders.module';

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

Criando os arquivos de users e orders:

nest g resource users
nest g resource orders

Service com Lógica de Negócio

Exemplo: Calcular o total de um pedido com base nos produtos:

// src/orders/orders.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { CreateOrderDto } from './dto/create-order.dto';

@Injectable()
export class OrdersService {
  constructor(private readonly prisma: PrismaService) {}

  async create(data: CreateOrderDto) {
    const items = await Promise.all(
      data.products.map(async ({ id, quantity }) => {
        const product = await this.prisma.product.findUnique({ where: { id } });
        return { productId: id, quantity, price: product.price * quantity };
      })
    );
    const total = items.reduce((sum, item) => sum + item.price, 0);
    return await this.prisma.order.create({
      data: {
        total,
        items: { create: items },
        userId: data.userId,
      },
    });
  }
}

Controller

Recebe requisições e chama métodos do Service:

// src/orders/orders.controller.ts
import { Controller, Post, Body } from '@nestjs/common';
import { OrdersService } from './orders.service';
import { CreateOrderDto } from './dto/create-order.dto';

@Controller('orders')
export class OrdersController {
  constructor(private readonly ordersService: OrdersService) {}

  @Post()
  create(@Body() dto: CreateOrderDto) {
    return this.ordersService.create(dto);
  }
}

Capturando e Tratando Erros Personalizados

Use @UseFilters() para capturar exceções do Prisma e retornar mensagens claras:

// src/prisma/prisma-error.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common';
import { Prisma } from '@prisma/client';

@Catch(Prisma.PrismaClientKnownRequestError)
export class PrismaErrorFilter implements ExceptionFilter {
  catch(exception: Prisma.PrismaClientKnownRequestError, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const status = HttpStatus.BAD_REQUEST;

    let message = 'An unexpected error occurred';
    if (exception.code === 'P2002') {
      message = 'Unique constraint violation';
    }

    response.status(status).json({ statusCode: status, message });
  }
}

Registre o filtro globalmente em main.ts:

app.useGlobalFilters(new PrismaErrorFilter());

Implementação do CRUD com Exemplos de Relacionamentos

Com o NestJS CLI, gere recursos para entidades como User, Order e Product:

nest generate resource users
nest generate resource orders

No Service, utilize métodos do Prisma para manipular relacionamentos:

// src/users/users.service.ts
async findOne(id: number) {
  return await this.prisma.user.findUnique({
    where: { id },
    include: { orders: true, profile: true },
  });
}

Conclusão

Este artigo demonstrou como criar uma API REST com NestJS e Prisma, cobrindo desde a definição de schema até a implementação de relacionamentos, validação de dados e tratamento de erros. A estrutura apresentada segue boas práticas de arquitetura, facilitando a manutenção e escalabilidade do projeto.

One comment

Leave a Reply

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *