🚀 Oferta especial: 60% OFF no CrazyStack - Últimas vagas!Garantir vaga →
Módulo 2 - Aula 2

Prisma e Database

Configure Prisma ORM com tRPC: schema design, migrations, relacionamentos e integração completa com PostgreSQL.

55 min
Intermediário
Database

🎯 Por que Prisma + tRPC é a combinação perfeita?

Type Safety End-to-End: Tipos do banco são automaticamente inferidos até o frontend.

Developer Experience: Autocompletar, migrations automáticas e debugging simplificado.

📊 Schema Prisma Completo

prisma/schema.prisma
// 📁 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
}

🔧 Configuração do Cliente Prisma

src/server/db/client.ts
// 📁 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();
});

🔗 Integração Prisma + tRPC

src/server/trpc/routers/post.ts
// 📁 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;
    }),
});

🗄️ Migrations e Seed Data

migration-commands.sh
# 📄 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

🌱 Seed Data

prisma/seed.ts
// 📁 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();
  });

✅ Vantagens Prisma + tRPC

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.