Saltar para o conteúdo
Prisma ORM: Guia Completo para Node.js
Node.js

Prisma ORM: Guia Completo para Node.js

15 de julho de 2024·Paulo de Paula

Prisma é o ORM mais adotado no ecossistema Node.js/TypeScript em 2024. Diferente de ORMs tradicionais como Sequelize, o Prisma gera tipos TypeScript a partir do schema — seu editor conhece cada campo, cada relação, cada retorno de query.

Instalação e configuração inicial

npm install prisma @prisma/client
npx prisma init --datasource-provider postgresql

Isso cria prisma/schema.prisma e adiciona DATABASE_URL no .env.

Definindo o schema

// prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model Usuario {
  id        String   @id @default(cuid())
  email     String   @unique
  nome      String
  role      Role     @default(USER)
  criadoEm DateTime @default(now())
  posts     Post[]
  perfil    Perfil?

  @@index([email])
  @@map("usuarios")
}

model Perfil {
  id        String   @id @default(cuid())
  bio       String?
  avatar    String?
  usuarioId String   @unique
  usuario   Usuario  @relation(fields: [usuarioId], references: [id], onDelete: Cascade)

  @@map("perfis")
}

model Post {
  id          String     @id @default(cuid())
  titulo      String
  conteudo    String
  publicado   Boolean    @default(false)
  criadoEm   DateTime   @default(now())
  atualizadoEm DateTime  @updatedAt
  autorId     String
  autor       Usuario    @relation(fields: [autorId], references: [id])
  tags        Tag[]
  comentarios Comentario[]

  @@index([autorId])
  @@index([publicado, criadoEm(sort: Desc)])
  @@map("posts")
}

model Tag {
  id    String @id @default(cuid())
  nome  String @unique
  posts Post[]

  @@map("tags")
}

model Comentario {
  id       String  @id @default(cuid())
  texto    String
  postId   String
  post     Post    @relation(fields: [postId], references: [id], onDelete: Cascade)

  @@map("comentarios")
}

enum Role {
  USER
  ADMIN
  EDITOR
}

Migrations

# Desenvolvimento: cria e aplica a migration
npx prisma migrate dev --name create-usuarios-posts

# Produção: aplica migrations pendentes sem criar novas
npx prisma migrate deploy

# Reseta o banco (desenvolvimento — DESTRÓI dados)
npx prisma migrate reset

# Visualiza o banco no browser
npx prisma studio

Instância singleton do PrismaClient

// lib/prisma.ts
import { PrismaClient } from '@prisma/client';

const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };

export const prisma =
  globalForPrisma.prisma ||
  new PrismaClient({
    log: process.env.NODE_ENV === 'development'
      ? ['query', 'error', 'warn']
      : ['error'],
  });

if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;

O singleton evita criar múltiplas conexões em desenvolvimento com hot reload (Next.js, nodemon).

CRUD com tipagem completa

import { prisma } from './lib/prisma';
import type { Usuario, Post } from '@prisma/client';

// Create
async function criarUsuario(email: string, nome: string): Promise<Usuario> {
  return prisma.usuario.create({
    data: { email, nome },
  });
}

// Read com seleção de campos
async function buscarUsuario(id: string) {
  return prisma.usuario.findUniqueOrThrow({
    where: { id },
    select: {
      id: true,
      nome: true,
      email: true,
      role: true,
      // Exclui automaticamente campos sensíveis não selecionados
    },
  });
}

// Update
async function publicarPost(postId: string): Promise<Post> {
  return prisma.post.update({
    where: { id: postId },
    data: { publicado: true },
  });
}

// Delete com soft delete pattern
async function deletarPost(postId: string) {
  // Cascade configurado no schema deleta comentários automaticamente
  return prisma.post.delete({ where: { id: postId } });
}

Queries com relações

// Inclui relações (equivale a JOIN)
const postsComAutor = await prisma.post.findMany({
  where: { publicado: true },
  include: {
    autor: {
      select: { nome: true, email: true },
    },
    tags: true,
    _count: { select: { comentarios: true } },
  },
  orderBy: { criadoEm: 'desc' },
  take: 10,
  skip: 0,
});

// Filtros aninhados
const usuariosAtivos = await prisma.usuario.findMany({
  where: {
    posts: {
      some: {
        publicado: true,
        criadoEm: { gte: new Date('2024-01-01') },
      },
    },
  },
  include: {
    _count: { select: { posts: true } },
  },
});

Transactions

// Transação interativa (para lógica condicional)
const resultado = await prisma.$transaction(async (tx) => {
  const usuario = await tx.usuario.create({
    data: { email: 'novo@email.com', nome: 'Novo Usuário' },
  });

  const perfil = await tx.perfil.create({
    data: { usuarioId: usuario.id, bio: 'Olá!' },
  });

  // Se qualquer operação falhar, tudo é revertido automaticamente
  return { usuario, perfil };
});

// Transação em lote (mais performática para operações independentes)
const [novoPost, tagAtualizada] = await prisma.$transaction([
  prisma.post.create({ data: { titulo: 'Novo', conteudo: '...', autorId: 'x' } }),
  prisma.tag.update({ where: { nome: 'nodejs' }, data: { nome: 'Node.js' } }),
]);

Queries raw para casos complexos

// SQL raw tipado
const resultado = await prisma.$queryRaw<{ mes: string; total: number }[]>`
  SELECT
    TO_CHAR(criado_em, 'YYYY-MM') AS mes,
    COUNT(*)::int                 AS total
  FROM posts
  WHERE publicado = true
  GROUP BY mes
  ORDER BY mes DESC
  LIMIT 12
`;

// Execute para INSERT/UPDATE/DELETE sem retorno tipado
await prisma.$executeRaw`
  UPDATE usuarios SET ultimo_acesso = NOW()
  WHERE id = ${usuarioId}
`;

Evitando o problema N+1

// ❌ N+1: 1 query para posts + N queries para cada autor
const posts = await prisma.post.findMany();
const postsComAutor = await Promise.all(
  posts.map(async (post) => ({
    ...post,
    autor: await prisma.usuario.findUnique({ where: { id: post.autorId } }),
  }))
);

// ✅ 2 queries com JOIN automático via include
const postsComAutor = await prisma.post.findMany({
  include: { autor: true },
});

Connection pooling em produção com PgBouncer

O Prisma abre uma connection pool interna, mas em ambientes serverless (Lambda, Vercel) cada instância abre seu próprio pool — podendo esgotar as conexões do PostgreSQL.

# Com Prisma Accelerate (serviço gerenciado da Prisma)
DATABASE_URL="prisma://accelerate.prisma-data.net/?api_key=..."

# Ou com PgBouncer próprio
DATABASE_URL="postgresql://user:pass@pgbouncer:6432/db?pgbouncer=true"
// Para serverless: cria cliente sem pool interno
const prisma = new PrismaClient({
  datasources: {
    db: { url: process.env.DATABASE_URL + '?connection_limit=1' },
  },
});

Seed de dados

// prisma/seed.ts
import { prisma } from '../lib/prisma';

async function main() {
  await prisma.usuario.upsert({
    where: { email: 'admin@blog.com' },
    update: {},
    create: {
      email: 'admin@blog.com',
      nome: 'Administrador',
      role: 'ADMIN',
      posts: {
        create: [
          { titulo: 'Primeiro post', conteudo: 'Conteúdo inicial', publicado: true },
        ],
      },
    },
  });
}

main()
  .catch(console.error)
  .finally(() => prisma.$disconnect());
// package.json
{
  "prisma": {
    "seed": "tsx prisma/seed.ts"
  }
}
npx prisma db seed

O Prisma é uma escolha sólida para projetos TypeScript. O gerador de tipos elimina uma classe inteira de bugs em tempo de desenvolvimento — campos com typo, relações inexistentes e tipos incorretos viram erros de compilação, não de runtime.