Configure Prisma ORM com tRPC: schema design, migrations, relacionamentos e integração completa com PostgreSQL.
Type Safety End-to-End: Tipos do banco são automaticamente inferidos até o frontend.
Developer Experience: Autocompletar, migrations automáticas e debugging simplificado.
// 📁 prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// 👤 Modelo de usuário
model User {
id String @id @default(cuid())
email String @unique
name String?
password String
role Role @default(USER)
// 📅 Timestamps
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// 🔗 Relacionamentos
posts Post[]
comments Comment[]
likes Like[]
@@map("users")
}
// 📝 Modelo de post
model Post {
id String @id @default(cuid())
title String
content String?
published Boolean @default(false)
slug String @unique
// 📅 Timestamps
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// 👤 Autor
authorId String
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
// 🔗 Relacionamentos
comments Comment[]
likes Like[]
tags PostTag[]
@@map("posts")
}
// 💬 Modelo de comentário
model Comment {
id String @id @default(cuid())
content String
// 📅 Timestamps
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// 🔗 Relacionamentos
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
postId String
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
// 💬 Comentários aninhados
parentId String?
parent Comment? @relation("CommentReplies", fields: [parentId], references: [id])
replies Comment[] @relation("CommentReplies")
@@map("comments")
}
// ❤️ Modelo de curtida
model Like {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
postId String
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
// 🛡️ Constraint: um usuário só pode curtir um post uma vez
@@unique([userId, postId])
@@map("likes")
}
// 🏷️ Modelo de tag
model Tag {
id String @id @default(cuid())
name String @unique
slug String @unique
// 🔗 Relacionamentos
posts PostTag[]
@@map("tags")
}
// 🔗 Tabela de relacionamento Many-to-Many
model PostTag {
postId String
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
tagId String
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
@@id([postId, tagId])
@@map("post_tags")
}
// 🎭 Enum para roles
enum Role {
USER
ADMIN
MODERATOR
}
// 📁 src/server/db/client.ts
import { PrismaClient } from '@prisma/client';
// 🌍 Configuração global do Prisma
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
// 🎯 Cliente Prisma com configurações otimizadas
export const prisma = globalForPrisma.prisma ??
new PrismaClient({
log:
process.env.NODE_ENV === 'development'
? ['query', 'error', 'warn']
: ['error'],
// 🚀 Configurações de performance
datasources: {
db: {
url: process.env.DATABASE_URL,
},
},
});
// 🔥 Evitar múltiplas instâncias em desenvolvimento
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma;
}
// 🛡️ Graceful shutdown
process.on('beforeExit', async () => {
await prisma.$disconnect();
});
// 📁 src/server/trpc/context.ts
import { type CreateNextContextOptions } from '@trpc/server/adapters/next';
import { prisma } from '../db/client';
// 🎯 Criar contexto para cada requisição
export const createContext = (opts: CreateNextContextOptions) => {
return {
req: opts.req,
res: opts.res,
prisma, // 📊 Cliente Prisma disponível em todos os procedures
};
};
export type Context = ReturnType<typeof createContext>;
// 📁 src/server/trpc/routers/post.ts
import { z } from 'zod';
import { router, publicProcedure, protectedProcedure } from '../trpc';
// 🎯 Router de posts com Prisma
export const postRouter = router({
// 📋 Listar posts com paginação
getAll: publicProcedure
.input(z.object({
limit: z.number().min(1).max(100).default(10),
cursor: z.string().optional(),
published: z.boolean().optional(),
}))
.query(async ({ input, ctx }) => {
const { limit, cursor, published } = input;
// 🔍 Query otimizada com relacionamentos
const posts = await ctx.prisma.post.findMany({
take: limit + 1,
cursor: cursor ? { id: cursor } : undefined,
where: {
published: published ?? true,
},
include: {
author: {
select: {
id: true,
name: true,
email: true,
},
},
tags: {
include: {
tag: true,
},
},
_count: {
select: {
comments: true,
likes: true,
},
},
},
orderBy: {
createdAt: 'desc',
},
});
let nextCursor: typeof cursor | undefined = undefined;
if (posts.length > limit) {
const nextItem = posts.pop();
nextCursor = nextItem!.id;
}
return {
posts,
nextCursor,
};
}),
// 📝 Criar novo post
create: protectedProcedure
.input(z.object({
title: z.string().min(1).max(255),
content: z.string().optional(),
published: z.boolean().default(false),
tagIds: z.array(z.string()).optional(),
}))
.mutation(async ({ input, ctx }) => {
const { title, content, published, tagIds } = input;
// 🎯 Gerar slug único
const slug = title
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)/g, '');
// 💾 Criar post com relacionamentos
const post = await ctx.prisma.post.create({
data: {
title,
content,
published,
slug,
authorId: ctx.user.id, // ✅ Vem do middleware de auth
tags: tagIds ? {
create: tagIds.map(tagId => ({
tag: {
connect: { id: tagId }
}
}))
} : undefined,
},
include: {
author: {
select: {
id: true,
name: true,
email: true,
},
},
tags: {
include: {
tag: true,
},
},
},
});
return post;
}),
});
# 📄 Comandos de Migration
# 🚀 Criar migration inicial
npx prisma migrate dev --name init
# 🔄 Aplicar mudanças no schema
npx prisma migrate dev --name add_comments_table
# 📊 Resetar database (cuidado em produção!)
npx prisma migrate reset
# 🌱 Executar seed
npx prisma db seed
# 📈 Gerar cliente após mudanças
npx prisma generate
# 📋 Visualizar database
npx prisma studio
// 📁 prisma/seed.ts
import { PrismaClient, Role } from '@prisma/client';
import bcrypt from 'bcryptjs';
const prisma = new PrismaClient();
async function main() {
console.log('🌱 Iniciando seed...');
// 🧹 Limpar dados existentes
await prisma.like.deleteMany();
await prisma.comment.deleteMany();
await prisma.postTag.deleteMany();
await prisma.post.deleteMany();
await prisma.tag.deleteMany();
await prisma.user.deleteMany();
// 👤 Criar usuários
const hashedPassword = await bcrypt.hash('123456', 10);
const admin = await prisma.user.create({
data: {
email: 'admin@example.com',
name: 'Admin User',
password: hashedPassword,
role: Role.ADMIN,
},
});
const user = await prisma.user.create({
data: {
email: 'user@example.com',
name: 'Regular User',
password: hashedPassword,
role: Role.USER,
},
});
// 🏷️ Criar tags
const reactTag = await prisma.tag.create({
data: {
name: 'React',
slug: 'react',
},
});
const nextjsTag = await prisma.tag.create({
data: {
name: 'Next.js',
slug: 'nextjs',
},
});
// 📝 Criar posts
const post1 = await prisma.post.create({
data: {
title: 'Introdução ao tRPC',
content: 'tRPC é uma biblioteca incrível para APIs type-safe...',
published: true,
slug: 'introducao-ao-trpc',
authorId: admin.id,
tags: {
create: [
{ tag: { connect: { id: reactTag.id } } },
{ tag: { connect: { id: nextjsTag.id } } },
],
},
},
});
// 💬 Criar comentários
await prisma.comment.create({
data: {
content: 'Excelente artigo sobre tRPC!',
userId: user.id,
postId: post1.id,
},
});
// ❤️ Criar likes
await prisma.like.create({
data: {
userId: user.id,
postId: post1.id,
},
});
console.log('✅ Seed concluído!');
}
main()
.catch((e) => {
console.error('❌ Erro no seed:', e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});
Type Safety Completo:Tipos do banco automaticamente disponíveis no frontend.
Queries Otimizadas:Include, select e where com autocompletar perfeito.
Migrations Automáticas:Mudanças no schema são refletidas automaticamente.
Relacionamentos Inteligentes:Joins complexos com sintaxe simples e performática.