Ir para o conteúdo
Framework com Framework2675 palavras

Como usar NestJS com Prisma: Guia Completo

Introdução

Se você trabalha com Node.js e TypeScript há algum tempo, já deve ter sentido a dor de configurar um ORM que realmente funcione bem com o ecossistema do seu framework. Conectar camadas de acesso a dados com injeção de dependências, manter type safety de ponta a ponta e ainda garantir que o projeto escale sem virar uma bagunça: esse é o desafio que a combinação NestJS com Prisma resolve de forma elegante.

NestJS é o framework Node.js que trouxe arquitetura opinada inspirada em Angular para o backend, com módulos, decorators e um sistema robusto de injeção de dependências. Prisma ORM, por sua vez, é um ORM type-safe que gera tipos automaticamente a partir de um schema declarativo, eliminando uma categoria inteira de erros em runtime. Quando você junta os dois, o resultado é um backend com tipagem forte do banco de dados até o controller, sem lacunas.

Neste guia, vamos cobrir o ciclo completo: da instalação e configuração até padrões avançados, boas práticas de segurança e os erros mais comuns que você vai encontrar no caminho. O conteúdo é voltado para desenvolvedores com pelo menos dois anos de experiência que querem adotar essa stack em projetos reais.

Por que Usar NestJS com Prisma

Type Safety de Ponta a Ponta

A principal vantagem dessa combinação é a eliminação de lacunas de tipagem. O Prisma gera tipos TypeScript diretamente do seu schema de banco de dados. O NestJS, por ser construído sobre TypeScript desde o início, consome esses tipos nativamente. Isso significa que, se você renomear uma coluna no banco, o compilador vai apontar todos os pontos do código que precisam de ajuste antes mesmo de você executar qualquer coisa.

Injeção de Dependências Nativa

O NestJS possui um container de IoC (Inversão de Controle) que gerencia o ciclo de vida dos serviços. O Prisma Client se encaixa perfeitamente nesse modelo: você cria um serviço dedicado, registra no módulo e injeta onde precisar. Isso facilita testes unitários, permite mockar o acesso ao banco e mantém a separação de responsabilidades limpa.

Casos de Uso Reais

Essa stack brilha em cenários como: APIs REST e GraphQL para aplicações SaaS, microsserviços que precisam de acesso tipado a bancos relacionais, plataformas multi-tenant com schemas complexos e sistemas que exigem migrações controladas em pipelines de CI/CD. Equipes que adotam NestJS com Prisma em produção reportam redução significativa de bugs relacionados a queries malformadas e ganho de produtividade na manutenção de código.

Setup Completo do Projeto

Criando o Projeto NestJS

Use uma versão de Node.js compatível tanto com o NestJS escolhido quanto com Prisma 6. Para a linha Node.js 22, Prisma 6 exige pelo menos 22.11.0; a versão mínima de TypeScript é 5.1.0. Registre as versões efetivamente utilizadas na validação do exemplo.

SHELL
# Instalar o CLI do NestJS globalmente
npm install -g @nestjs/cli
 
# Criar um novo projeto
nest new meu-projeto-api
 
# Entrar no diretório
cd meu-projeto-api

O CLI vai perguntar qual gerenciador de pacotes você prefere. Escolha npm, yarn ou pnpm conforme a preferência do seu time.

Instalando e Configurando o Prisma

Agora vamos adicionar o Prisma ao projeto:

Este guia usa Prisma ORM 6.14.0 com prisma-client-js e PostgreSQL. Instale prisma e @prisma/client na mesma versão exata e versione o package-lock.json. Os exemplos a seguir pertencem a essa configuração; a atualização para Prisma 7 requer adaptar a configuração e a inicialização do client.

Bash
npm install --save-dev --save-exact [email protected]
npm install --save-exact @prisma/[email protected]
npx prisma init --datasource-provider postgresql

Esse comando cria dois artefatos importantes: o arquivo prisma/schema.prisma e um .env com a variável DATABASE_URL. Configure a URL de conexão no .env:

SHELL
DATABASE_URL="postgresql://usuario:senha@localhost:5432/meu_banco?schema=public"

Uma nota sobre segurança: aplique o princípio de menor privilégio nas credenciais de banco. Nunca use o superusuário do PostgreSQL na aplicação. Crie um usuário específico com apenas as permissões necessárias (SELECT, INSERT, UPDATE, DELETE nas tabelas da aplicação) e reserve credenciais com permissão de DDL exclusivamente para o processo de migração no CI/CD.

Definindo o Schema do Prisma

Vamos criar um schema realista para uma API de gerenciamento de tarefas:

Depois da inicialização, manter explicitamente o generator client com provider = "prisma-client-js" e o datasource db do artigo. O restante do arquivo deve ser o schema apresentado no guia, incluindo a alteração de Task abaixo. Não misturar o template de outra versão com esse schema.

TYPESCRIPT
// prisma/schema.prisma
 
generator client {
  provider = "prisma-client-js"
}
 
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}
 
model User {
  id        String   @id @default(cuid())
  email     String   @unique
  name      String
  role      Role     @default(MEMBER)
  tasks     Task[]
  createdAt DateTime @default(now()) @map("created_at")
  updatedAt DateTime @updatedAt @map("updated_at")
 
  @@map("users")
}
 
model Task {
  id           String      @id @default(cuid())
  title        String
  description  String?
  status       TaskStatus  @default(TODO)
  priority     Int         @default(0)
  authorId     String      @map("author_id")
  author       User        @relation(fields: [authorId], references: [id], onDelete: Cascade)
  createdAt    DateTime    @default(now()) @map("created_at")
  updatedAt    DateTime    @updatedAt @map("updated_at")
  deletedAt    DateTime?   @map("deleted_at")
 
  @@index([authorId])
  @@index([status])
  @@map("tasks")
}
 
enum Role {
  ADMIN
  MEMBER
}
 
enum TaskStatus {
  TODO
  IN_PROGRESS
  DONE
}

Observe o uso de @@map para manter a convenção snake_case no banco enquanto o código TypeScript usa camelCase. Isso é uma boa prática que mantém ambos os mundos felizes.

Executando as Migrações

Para um banco que já contém tarefas, a coluna nullable permite preservar os registros existentes com deleted_at = NULL. Revisar o SQL gerado antes de qualquer aplicação. Para uma instalação nova do tutorial, incluir o campo desde a migração inicial, em vez de exigir uma segunda migração sem necessidade. A distinção entre migrate dev e a aplicação de migrações em produção está no guia oficial de migrações.

Bash
npx prisma validate
npx prisma migrate dev --name add_task_deleted_at
npx prisma generate
SHELL
# Criar e aplicar a primeira migração
npx prisma migrate dev --name init
 
# Gerar o Prisma Client (acontece automaticamente com migrate dev, mas é bom conhecer)
npx prisma generate

O comando migrate dev cria o arquivo SQL de migração, aplica no banco e regenera o client. Em produção, o comando é diferente:

SHELL
# Em produção ou CI/CD: apenas aplica migrações pendentes
npx prisma migrate deploy

Exemplo Prático: Integração Completa

Criando o PrismaService

O padrão recomendado é encapsular o Prisma Client em um serviço NestJS dedicado:

TYPESCRIPT
// src/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
{
  constructor() {
    super({
      log:
        process.env.NODE_ENV === 'development'
          ? ['query', 'info', 'warn', 'error']
          : ['error'],
    });
  }
 
  async onModuleInit(): Promise<void> {
    await this.$connect();
  }
 
  async onModuleDestroy(): Promise<void> {
    await this.$disconnect();
  }
}

Implementar OnModuleInit e OnModuleDestroy garante que a conexão com o banco é gerenciada corretamente pelo ciclo de vida do NestJS. Isso é especialmente importante em ambientes serverless ou quando você faz hot reload durante o desenvolvimento.

Criando o PrismaModule Global

TYPESCRIPT
// src/prisma/prisma.module.ts
import { Global, Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';
 
@Global()
@Module({
  providers: [PrismaService],
  exports: [PrismaService],
})
export class PrismaModule {}

O decorator @Global() faz com que o módulo esteja disponível em toda a aplicação sem precisar importá-lo em cada módulo. Registre-o no AppModule:

TYPESCRIPT
// src/app.module.ts
import { Module } from '@nestjs/common';
import { PrismaModule } from './prisma/prisma.module';
import { TasksModule } from './tasks/tasks.module';
import { UsersModule } from './users/users.module';
 
@Module({
  imports: [PrismaModule, TasksModule, UsersModule],
})
export class AppModule {}

Implementando o CRUD de Tasks

Agora vamos ao módulo de tarefas com DTOs validados e um serviço completo:

TYPESCRIPT
// src/tasks/dto/create-task.dto.ts
import { IsString, IsOptional, IsInt, Min, Max } from 'class-validator';
 
export class CreateTaskDto {
  @IsString()
  title: string;
 
  @IsString()
  @IsOptional()
  description?: string;
 
  @IsInt()
  @Min(0)
  @Max(5)
  @IsOptional()
  priority?: number;
 
  @IsString()
  authorId: string;
}
TYPESCRIPT
import { Injectable, NotFoundException } from '@nestjs/common';
import { Prisma, TaskStatus } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import { CreateTaskDto } from './dto/create-task.dto';
 
@Injectable()
export class TasksService {
  constructor(private readonly prisma: PrismaService) {}
 
  async create(dto: CreateTaskDto) {
    return this.prisma.task.create({
      data: {
        title: dto.title,
        description: dto.description,
        priority: dto.priority ?? 0,
        authorId: dto.authorId,
      },
      include: { author: true },
    });
  }
 
  async findAll(status?: TaskStatus, page = 1, limit = 20) {
    const skip = (page - 1) * limit;
    const where: Prisma.TaskWhereInput = {
      deletedAt: null,
      ...(status ? { status } : {}),
    };
 
    const [tasks, total] = await this.prisma.$transaction([
      this.prisma.task.findMany({
        where,
        include: {
          author: { select: { id: true, name: true, email: true } },
        },
        orderBy: { createdAt: 'desc' },
        skip,
        take: limit,
      }),
      this.prisma.task.count({ where }),
    ]);
 
    return {
      data: tasks,
      meta: { total, page, limit, totalPages: Math.ceil(total / limit) },
    };
  }
 
  async findOne(id: string) {
    const task = await this.prisma.task.findUnique({
      where: { id, deletedAt: null },
      include: { author: true },
    });
 
    if (!task) {
      throw new NotFoundException(`Task com id ${id} não encontrada`);
    }
 
    return task;
  }
 
  async updateStatus(id: string, status: TaskStatus) {
    return this.updateActiveTask(id, { status });
  }
 
  async remove(id: string) {
    return this.updateActiveTask(id, { deletedAt: new Date() });
  }
 
  private async updateActiveTask(id: string, data: Prisma.TaskUpdateInput) {
    try {
      return await this.prisma.task.update({
        where: { id, deletedAt: null },
        data,
      });
    } catch (error) {
      if (
        error instanceof Prisma.PrismaClientKnownRequestError &&
        error.code === 'P2025'
      ) {
        throw new NotFoundException(`Task com id ${id} não encontrada`);
      }
 
      throw error;
    }
  }
}
TYPESCRIPT
// src/tasks/tasks.controller.ts
import {
  Controller,
  Get,
  Post,
  Patch,
  Delete,
  Body,
  Param,
  Query,
} from '@nestjs/common';
import { TasksService } from './tasks.service';
import { CreateTaskDto } from './dto/create-task.dto';
import { TaskStatus } from '@prisma/client';
 
@Controller('tasks')
export class TasksController {
  constructor(private readonly tasksService: TasksService) {}
 
  @Post()
  create(@Body() dto: CreateTaskDto) {
    return this.tasksService.create(dto);
  }
 
  @Get()
  findAll(
    @Query('status') status?: TaskStatus,
    @Query('page') page?: string,
    @Query('limit') limit?: string,
  ) {
    return this.tasksService.findAll(
      status,
      page ? parseInt(page, 10) : 1,
      limit ? parseInt(limit, 10) : 20,
    );
  }
 
  @Get(':id')
  findOne(@Param('id') id: string) {
    return this.tasksService.findOne(id);
  }
 
  @Patch(':id/status')
  updateStatus(@Param('id') id: string, @Body('status') status: TaskStatus) {
    return this.tasksService.updateStatus(id, status);
  }
 
  @Delete(':id')
  remove(@Param('id') id: string) {
    return this.tasksService.remove(id);
  }
}

Note o uso de $transaction no findAll para buscar dados e contagem em uma única transação, garantindo consistência na paginação. Esse é um padrão que você vai usar constantemente em APIs de produção.

Boas Práticas para Produção

Abordagem Moderna: Prisma com Driver Adapters

A partir do Prisma 5.4+, existe o recurso de Driver Adapters que permite usar drivers nativos do Node.js (como pg, @neondatabase/serverless ou @planetscale/database) diretamente. Essa é a abordagem atual recomendada para cenários serverless e edge runtimes. A abordagem legada, que depende exclusivamente do Query Engine binário do Prisma, ainda funciona, mas os driver adapters oferecem menor overhead e melhor compatibilidade com plataformas como Cloudflare Workers e Vercel Edge Functions.

Soft delete explícito de tarefas

O modelo Task possui deletedAt. O método remove marca a data de exclusão; findAll, findOne e a contagem consideram apenas tarefas com deletedAt nulo. updateStatus também exige uma tarefa ativa. Esta regra está nos métodos de TasksService e não intercepta automaticamente todas as consultas do Prisma. Consultas diretas, includes de tarefas em outros serviços e escritas aninhadas precisam adotar a mesma política quando forem implementadas.

A exclusão lógica demonstrada cobre tarefas. O modelo User não recebeu soft delete. A relação mantém onDelete: Cascade, portanto uma exclusão física de User no banco pode remover fisicamente suas tarefas. Uma política de retenção para usuários e tarefas exige rever essa relação e todos os caminhos de exclusão.

Testes com Prisma

Para testes unitários, mocke o PrismaService. Para testes de integração, use um banco de teste dedicado:

SHELL
# Rodar migrações no banco de teste
DATABASE_URL="postgresql://user:pass@localhost:5432/test_db" npx prisma migrate deploy

Cleanup e Destruição

Para resetar o banco durante o desenvolvimento:

SHELL
# Reset completo: drop do banco, recria e aplica todas as migrações
npx prisma migrate reset
 
# Apenas visualizar o estado das migrações
npx prisma migrate status

Em produção, se precisar reverter, o Prisma não tem rollback automático. Você deve criar uma nova migração que desfaça as alterações. Isso é intencional: migrações forward-only são mais seguras.

Para remover o Prisma completamente de um projeto:

SHELL
# Remover dependências
npm uninstall prisma @prisma/client
 
# Remover arquivos gerados
rm -rf prisma/ node_modules/.prisma

Erros Comuns e Soluções

Erro 1: PrismaClientInitializationError: Can't reach database server

Diagnóstico: a aplicação não consegue se conectar ao banco de dados.

Solução: verifique se a DATABASE_URL no .env está correta e se o banco está acessível. Teste a conexão diretamente:

SHELL
npx prisma db pull

Se falhar, o problema é de rede ou credenciais, não do Prisma.

Erro 2: Error: @prisma/client did not initialize yet

Diagnóstico: o código tenta usar o Prisma Client antes de executar prisma generate.

Solução: execute npx prisma generate após qualquer alteração no schema. Adicione esse comando ao script de build:

JSON
{
  "scripts": {
    "build": "prisma generate && nest build"
  }
}

Erro 3: P2002: Unique constraint failed on the fields

Diagnóstico: tentativa de inserir um registro com valor duplicado em campo único (como email).

Solução: trate o erro no serviço com um catch específico:

TYPESCRIPT
import { Prisma } from '@prisma/client';
 
try {
  return await this.prisma.user.create({ data: dto });
} catch (error) {
  if (
    error instanceof Prisma.PrismaClientKnownRequestError &&
    error.code === 'P2002'
  ) {
    throw new ConflictException('Email já cadastrado');
  }
  throw error;
}

Erro 4: P2025: Record to update not found

Diagnóstico: tentativa de atualizar ou deletar um registro que não existe.

Solução: sempre verifique a existência do registro antes de operar sobre ele (como fizemos no findOne do exemplo) ou trate o erro P2025 diretamente. Crie um exception filter global para erros do Prisma para centralizar essa lógica.

Erro 5: Migration failed: drift detected

Diagnóstico: o estado do banco divergiu do esperado pelas migrações. Isso acontece quando alguém altera o banco manualmente.

Solução: em desenvolvimento, use npx prisma migrate reset para recomeçar. Em produção, resolva a drift criando uma migração que alinha o schema. Também é possível usar npx prisma db pull para sincronizar o schema com o estado atual do banco e então criar uma nova baseline.

Erro 6: Timeout em Queries com Muitos Dados

Diagnóstico: queries que buscam grandes volumes travam ou retornam timeout.

Solução: sempre use paginação (como no exemplo do findAll), adicione índices no schema para campos usados em where e orderBy, e configure o timeout do Prisma Client:

TYPESCRIPT
super({
  datasources: {
    db: {
      url: process.env.DATABASE_URL,
    },
  },
  // Timeout de 10 segundos para queries
  // Use com cuidado: prefira otimizar a query
});

Erro 7: Cannot find module '.prisma/client'

Diagnóstico: o diretório node_modules/.prisma não foi gerado, comum após reinstalar dependências ou em ambientes de CI/CD.

Solução: adicione prisma generate como script postinstall:

JSON
{
  "scripts": {
    "postinstall": "prisma generate"
  }
}

Isso garante que o client é regenerado automaticamente após cada npm install.

Conclusão

A integração entre NestJS e Prisma cria uma experiência de desenvolvimento que é difícil de superar no ecossistema Node.js. A type safety do schema até o controller, o sistema de migrações controlado e a injeção de dependências nativa do NestJS formam uma base sólida para aplicações de qualquer porte.

Os pontos fundamentais para levar deste guia: encapsule o Prisma Client em um serviço dedicado com gerenciamento de ciclo de vida, use módulos globais para evitar imports repetitivos, trate erros do Prisma de forma centralizada e nunca esqueça de adicionar prisma generate ao pipeline de build.

Se você quer entender como o Prisma se compara a outras opções de ORM, especialmente em termos de performance, pode aprofundar esse tema no nosso comparativo "Prisma ORM vs Drizzle ORM: Benchmark Real em 2025". Também vale explorar em outro guia como configurar testes de integração completos com banco de dados usando containers Docker e Testcontainers, um tema que complementa diretamente o que abordamos aqui.

O ecossistema TypeScript no backend amadureceu muito nos últimos anos, e a combinação NestJS com Prisma é uma das provas mais concretas disso. Se você ainda está usando queries manuais ou ORMs sem type safety, considere fazer a migração. Seu futuro eu, debugando código em produção às duas da manhã, vai agradecer.

Termos do Glossário

Guias Relacionados

Explore mais guias técnicos

Mais de 100 guias práticos de integração entre frameworks, ferramentas e plataformas.

Ver todos os guias