Ir para o conteúdo
Framework com Framework2373 palavras

Como usar Next.js com Prisma: Guia Completo

Introdução

Se você trabalha com desenvolvimento web em TypeScript, provavelmente já se deparou com a necessidade de conectar sua aplicação Next.js a um banco de dados relacional. A pergunta que surge naturalmente é: qual a melhor forma de fazer isso sem abrir mão de type safety, produtividade e performance?

A resposta curta: Prisma ORM.

A combinação de Next.js com Prisma se tornou uma das stacks mais populares do ecossistema TypeScript, e não por acaso. Enquanto o Next.js cuida de toda a camada de renderização, roteamento e API, o Prisma oferece uma camada de acesso a dados com tipagem automática gerada a partir do schema do banco. O resultado é uma experiência de desenvolvimento onde erros de query são capturados em tempo de compilação, não em produção às 3 da manhã.

Neste guia, vou cobrir desde a configuração inicial até padrões avançados que uso em projetos reais. Se você já tem pelo menos dois anos de experiência com desenvolvimento web e quer elevar a qualidade das suas aplicações full-stack, este conteúdo foi escrito para você.

A fronteira é a mesma em qualquer arquitetura: o navegador (React) chama uma API ou uma Server Action; a API ou a action chama o Prisma; o Prisma chama o banco. O Prisma Client nunca é importado em código que roda no navegador, e a DATABASE_URL nunca chega ao bundle do cliente. Este guia absorveu o antigo guia de Prisma com React: se o seu frontend é React com Vite, React Native ou qualquer SPA sem Next.js, o Prisma fica num backend Node.js e o cliente conversa com ele por HTTP. O exemplo 2, de API Route com busca paginada, é exatamente esse contrato, e vale igual para um Express ou Fastify.

Por que usar Next.js com Prisma juntos

Type safety de ponta a ponta

O maior benefício dessa combinação é a tipagem contínua. O Prisma gera tipos TypeScript automaticamente a partir do seu schema declarativo. Quando você usa esses tipos dentro de Server Components ou Route Handlers do Next.js, o compilador do TypeScript valida tudo: desde os campos que você seleciona na query até o formato do objeto que chega ao componente de UI. Qualquer alteração no schema do banco propaga erros de compilação por toda a aplicação, tornando refatorações muito mais seguras.

Produtividade com Server Components

Desde o Next.js 13 (e aprimorado nas versões seguintes), os Server Components permitem buscar dados diretamente no servidor sem precisar criar endpoints intermediários. Isso significa que você pode chamar o Prisma Client dentro de um componente React que roda exclusivamente no servidor. Sem API REST, sem camada extra, sem serialização manual. Se você quer se aprofundar nesse modelo, recomendo a leitura do nosso post sobre Server Components no Next.js 16.

Migrações declarativas

O Prisma Migrate permite versionar o schema do banco de dados como código. Você altera o arquivo schema.prisma, roda um comando, e o Prisma gera a migração SQL correspondente. Isso elimina a necessidade de escrever SQL de migração manualmente na maioria dos casos.

Casos de uso onde essa combinação brilha

Essa stack se destaca em cenários específicos: dashboards administrativos com múltiplas entidades relacionadas, plataformas SaaS com modelos de dados complexos, aplicações de e-commerce com catálogos e pedidos, e sistemas de gerenciamento de conteúdo customizados. Em todos esses casos, a tipagem automática e o query builder do Prisma reduzem drasticamente a quantidade de bugs relacionados a acesso a dados.

Setup passo a passo: Next.js com Prisma

Criando o projeto Next.js

Comece criando um projeto Next.js com o App Router habilitado:

TYPESCRIPT
// No terminal, execute:
// npx create-next-app@latest meu-projeto --typescript --app --src-dir
// cd meu-projeto
// npm install prisma --save-dev
// npm install @prisma/client
// npx prisma init

O comando prisma init cria dois artefatos: o arquivo prisma/schema.prisma e um .env com a variável DATABASE_URL.

Configurando o schema do Prisma

Vamos usar um exemplo realista: uma aplicação de gerenciamento de posts com autores e categorias. Edite o arquivo prisma/schema.prisma:

TYPESCRIPT
// prisma/schema.prisma
 
// generator e datasource são blocos de configuração do Prisma
// Aqui estamos usando PostgreSQL, mas você pode trocar por mysql ou sqlite
 
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(AUTHOR)
  posts     Post[]
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}
 
model Post {
  id          String     @id @default(cuid())
  title       String
  slug        String     @unique
  content     String
  published   Boolean    @default(false)
  author      User       @relation(fields: [authorId], references: [id])
  authorId    String
  categories  Category[]
  createdAt   DateTime   @default(now())
  updatedAt   DateTime   @updatedAt
}
 
model Category {
  id    String @id @default(cuid())
  name  String @unique
  slug  String @unique
  posts Post[]
}
 
enum Role {
  ADMIN
  AUTHOR
  READER
}

Após definir o schema, execute as migrações:

TYPESCRIPT
// No terminal:
// npx prisma migrate dev --name init
// Isso cria as tabelas no banco e gera o Prisma Client tipado

Configurando o Prisma Client como singleton

Este é um dos pontos mais importantes e frequentemente ignorado. Em desenvolvimento, o Next.js faz hot reload do código, o que pode criar múltiplas instâncias do Prisma Client e esgotar as conexões do banco. A solução é criar um singleton:

TYPESCRIPT
// src/lib/prisma.ts
 
import { PrismaClient } from "@prisma/client";
 
const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined;
};
 
export const prisma = globalForPrisma.prisma ?? new PrismaClient({
  log: process.env.NODE_ENV === "development" ? ["query", "warn", "error"] : ["error"],
});
 
if (process.env.NODE_ENV !== "production") {
  globalForPrisma.prisma = prisma;
}

Esse padrão armazena a instância do Prisma Client no objeto globalThis, que persiste entre reloads em desenvolvimento. Em produção, uma única instância é criada normalmente. O parâmetro log é configurado para exibir queries no console durante o desenvolvimento, facilitando o debug.

Exemplo prático 1: CRUD completo com Server Components e Server Actions

Listando posts com Server Components

Vamos criar uma página que lista posts publicados com seus autores. Note que não precisamos de nenhum endpoint de API: o Server Component acessa o Prisma diretamente.

TYPESCRIPT
// src/app/posts/page.tsx
 
import { prisma } from "@/lib/prisma";
import Link from "next/link";
 
interface PostWithAuthor {
  id: string;
  title: string;
  slug: string;
  published: boolean;
  createdAt: Date;
  author: {
    name: string;
    email: string;
  };
  categories: {
    id: string;
    name: string;
    slug: string;
  }[];
}
 
async function getPosts(): Promise<PostWithAuthor[]> {
  const posts = await prisma.post.findMany({
    where: { published: true },
    include: {
      author: {
        select: {
          name: true,
          email: true,
        },
      },
      categories: {
        select: {
          id: true,
          name: true,
          slug: true,
        },
      },
    },
    orderBy: { createdAt: "desc" },
    take: 20,
  });
 
  return posts;
}
 
export default async function PostsPage() {
  const posts = await getPosts();
 
  return (
    <main>
      <h1>Posts Publicados</h1>
      {posts.length === 0 && <p>Nenhum post encontrado.</p>}
      <ul>
        {posts.map((post) => (
          <li key={post.id}>
            <Link href={`/posts/${post.slug}`}>
              <h2>{post.title}</h2>
            </Link>
            <p>Por {post.author.name}</p>
            <div>
              {post.categories.map((cat) => (
                <span key={cat.id}>{cat.name}</span>
              ))}
            </div>
            <time dateTime={post.createdAt.toISOString()}>
              {post.createdAt.toLocaleDateString("pt-BR")}
            </time>
          </li>
        ))}
      </ul>
    </main>
  );
}

Observe como o Prisma infere automaticamente o tipo de retorno de findMany com base nos campos selecionados via include e select. Ao passar o mouse sobre a variável posts no seu editor, você verá o tipo completo gerado. Se alguém remover o campo name do modelo User no schema, o TypeScript imediatamente aponta o erro neste componente.

Criando posts com Server Actions

Agora vamos implementar a criação de posts usando Server Actions, funcionalidade estável no Next.js a partir da versão 14:

TYPESCRIPT
// src/app/posts/new/actions.ts
 
"use server";
 
import { prisma } from "@/lib/prisma";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
 
interface CreatePostInput {
  title: string;
  content: string;
  authorId: string;
  categoryIds: string[];
}
 
function generateSlug(title: string): string {
  return title
    .toLowerCase()
    .normalize("NFD")
    .replace(/[\u0300-\u036f]/g, "")
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/(^-|-$)/g, "");
}
 
export async function createPost(formData: FormData) {
  const title = formData.get("title") as string;
  const content = formData.get("content") as string;
  const authorId = formData.get("authorId") as string;
  const categoryIds = formData.getAll("categoryIds") as string[];
 
  // Validação básica
  if (!title || title.length < 3) {
    throw new Error("O título precisa ter pelo menos 3 caracteres.");
  }
 
  if (!content || content.length < 10) {
    throw new Error("O conteúdo precisa ter pelo menos 10 caracteres.");
  }
 
  const slug = generateSlug(title);
 
  // Verifica se o slug já existe
  const existingPost = await prisma.post.findUnique({
    where: { slug },
  });
 
  if (existingPost) {
    throw new Error("Já existe um post com um título muito similar.");
  }
 
  await prisma.post.create({
    data: {
      title,
      slug,
      content,
      authorId,
      categories: {
        connect: categoryIds.map((id) => ({ id })),
      },
    },
  });
 
  revalidatePath("/posts");
  redirect("/posts");
}
 
export async function togglePostPublished(postId: string) {
  const post = await prisma.post.findUnique({
    where: { id: postId },
    select: { published: true },
  });
 
  if (!post) {
    throw new Error("Post não encontrado.");
  }
 
  await prisma.post.update({
    where: { id: postId },
    data: { published: !post.published },
  });
 
  revalidatePath("/posts");
}

Essas Server Actions rodam exclusivamente no servidor. O formulário no cliente envia os dados via formData, e toda a lógica de persistência acontece no servidor com acesso direto ao Prisma. Não há exposição de endpoints, não há fetch manual, e a tipagem permanece consistente.

Exemplo prático 2: API Route com busca paginada e filtros

Nem tudo precisa ser Server Action. Para integrações com apps mobile, webhooks, um frontend React separado (Vite, React Native ou qualquer SPA) ou qualquer cliente externo, API Routes continuam sendo essenciais. Este exemplo cria um endpoint de busca paginada com filtros tipados.

TYPESCRIPT
// app/api/posts/route.ts
 
import { prisma } from "@/lib/prisma";
import { NextRequest, NextResponse } from "next/server";
import { Prisma } from "@prisma/client";
 
interface PostQueryParams {
  page: number;
  limit: number;
  search?: string;
  published?: boolean;
}
 
function parseQueryParams(req: NextRequest): PostQueryParams {
  const url = req.nextUrl;
 
  const page = Math.max(1, Number(url.searchParams.get("page")) || 1);
  const limit = Math.min(50, Math.max(1, Number(url.searchParams.get("limit")) || 10));
  const search = url.searchParams.get("search") ?? undefined;
  const publishedParam = url.searchParams.get("published");
 
  const published =
    publishedParam === "true" ? true : publishedParam === "false" ? false : undefined;
 
  return { page, limit, search, published };
}
 
export async function GET(req: NextRequest) {
  try {
    const { page, limit, search, published } = parseQueryParams(req);
    const skip = (page - 1) * limit;
 
    const where: Prisma.PostWhereInput = {};
 
    if (search) {
      where.OR = [
        { title: { contains: search, mode: "insensitive" } },
        { content: { contains: search, mode: "insensitive" } },
      ];
    }
 
    if (published !== undefined) {
      where.published = published;
    }
 
    const [posts, total] = await prisma.$transaction([
      prisma.post.findMany({
        where,
        skip,
        take: limit,
        orderBy: { createdAt: "desc" },
        include: {
          author: {
            select: { id: true, name: true },
          },
        },
      }),
      prisma.post.count({ where }),
    ]);
 
    return NextResponse.json({
      data: posts,
      meta: {
        page,
        limit,
        total,
        totalPages: Math.ceil(total / limit),
      },
    });
  } catch (err) {
    console.error("Erro ao buscar posts:", err);
    return NextResponse.json(
      { error: "Erro interno ao buscar posts." },
      { status: 500 }
    );
  }
}

Esse endpoint pode ser consumido de qualquer Client Component usando fetch, React Query, SWR ou qualquer biblioteca de sua preferência. O uso de Prisma.PostWhereInput garante que os filtros sejam type-safe: se você tentar filtrar por um campo que não existe no modelo, o TypeScript acusa o erro em tempo de compilação.

O $transaction com duas operações paralelas (busca + contagem) garante consistência na paginação e evita race conditions entre a query de dados e a query de total.

Boas práticas para Next.js com Prisma em produção

Evite o problema N+1 com include e select

Um dos erros mais comuns é buscar dados relacionados dentro de loops. Sempre use include ou select para carregar relações em uma única query. O Prisma traduz isso em JOINs ou queries batched, evitando dezenas de round trips ao banco.

Use select para retornar apenas o necessário

Em vez de retornar todos os campos de um modelo, especifique exatamente o que precisa com select. Isso reduz o tráfego de dados e melhora a performance, especialmente em modelos com campos de texto longo.

Cuidado com o Prisma Client em Edge Runtime

Se você usa Edge Runtime no Next.js (middleware ou rotas edge), saiba que o Prisma Client padrão não funciona nesse ambiente. Você precisa usar o Prisma Accelerate ou o driver adapter para conectar via HTTP. Este é um ponto que gera confusão frequente em projetos novos.

Valide dados antes de enviar ao Prisma

O Prisma não é uma camada de validação. Use bibliotecas como Zod para validar inputs antes de chamar qualquer método do Prisma Client. Isso previne erros de constraint no banco e produz mensagens de erro mais amigáveis para o usuário.

Monitore queries em desenvolvimento

A configuração de log: ["query"] que incluímos no singleton é essencial durante o desenvolvimento. Ela permite identificar queries lentas, N+1 acidentais e joins desnecessários antes que cheguem a produção. Se quiser entender melhor como a performance do Prisma se compara com outras soluções, confira nosso benchmark real entre Prisma e Drizzle.

Organize queries em módulos de serviço

À medida que o projeto cresce, evite espalhar chamadas ao Prisma por dezenas de componentes. Crie módulos de serviço (como src/services/post-service.ts) que encapsulam as queries. Isso facilita testes, reutilização e manutenção.

Configure o connection pool adequadamente

Em ambientes serverless (como Vercel), cada invocação pode criar uma nova conexão. Configure o connection_limit na URL do banco e considere usar o Prisma Accelerate ou PgBouncer para gerenciar o pool de conexões de forma eficiente.

Conclusão

A integração entre Next.js e Prisma oferece uma experiência de desenvolvimento full-stack em TypeScript que é difícil de superar. O schema declarativo do Prisma alimenta a geração automática de tipos, que por sua vez garante type safety desde a query no banco até o componente React que renderiza os dados. Server Components e Server Actions eliminam a necessidade de camadas intermediárias de API na maioria dos casos, simplificando a arquitetura.

Os pontos fundamentais para levar deste guia: sempre use o padrão singleton para o Prisma Client, prefira select sobre include completo quando possível, valide dados com Zod antes de persistir, e monitore suas queries durante o desenvolvimento.

Se você quer otimizar ainda mais o tempo de build da sua aplicação Next.js com Prisma, vale conferir nosso tutorial avançado sobre Turbopack, que pode reduzir significativamente o ciclo de feedback durante o desenvolvimento.

A stack está madura, a documentação é excelente, e a comunidade é ativa. Se você ainda não experimentou essa combinação, este é o momento.

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