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:
- Schema do Prisma: Definição de modelos e relacionamentos.
- Relacionamentos: One-to-One, One-to-Many e Many-to-Many.
- DTOs (Data Transfer Objects): Validação e estruturação de dados.
- Tratamento de Erros Personalizados: Capturar exceções e retornar mensagens claras ao cliente.
- 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.
- 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);
}
- 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