Next.js+Prisma + PostgreSQL
Stack Integration

Next.js + Prisma + PostgreSQL Integration Guide

Prisma gives Next.js App Router a type-safe ORM layer over PostgreSQL — schema-driven development, auto-generated types, and migrations that version-control your database alongside your code.

Use Cases
  1. Type-safe database queries in Next.js Server Components and Route Handlers
  2. Database migrations managed with Prisma Migrate alongside git history
  3. Complex relational queries with Prisma's fluent API replacing raw SQL
  4. Seeding development databases with typed seed scripts
Implementation

Initialize Prisma with `prisma init`. Define your schema in `prisma/schema.prisma` — Prisma generates TypeScript types from it. Instantiate PrismaClient once in a singleton module to avoid exhausting connection pools in development. In Next.js, always import the PrismaClient singleton in Server Components and Route Handlers, never in client components. Run `prisma migrate dev` for local development and `prisma migrate deploy` in CI.

In detail

Why Prisma over Drizzle, Kysely, or raw SQL

Next.js App Router projects have three credible ORM choices in 2026: Prisma, Drizzle, and Kysely. Prisma wins for product teams shipping fast because the developer experience is unmatched — `prisma generate` produces a typed client from a single schema file, autocomplete works on every relation, and the studio GUI lets non-engineers inspect data without writing SQL. The cost is bundle size (Prisma's query engine is ~15 MB) and a small runtime overhead per query. Drizzle is leaner and ships closer to raw SQL with type inference, ideal for performance-critical paths or edge deployments. Kysely is a SQL query builder, not an ORM — choose it when you want type safety but reject the migration/schema-management abstractions. For a Next.js SaaS MVP where you want to ship features instead of debug query builders, Prisma is the default.

Setup — the minimum viable Prisma + Next.js project

Install with `npm install prisma @prisma/client` then run `npx prisma init --datasource-provider postgresql`. This creates `prisma/schema.prisma` and a `.env` file expecting `DATABASE_URL`. Define models in the schema, then `npx prisma migrate dev --name init` generates the first migration SQL file and applies it to your local database. Create `lib/prisma.ts` exporting a singleton: instantiate `new PrismaClient()` once, attach it to `globalThis` in development so hot reload does not create a new client on every save. Import this singleton in Server Components, Server Actions, and Route Handlers. Never import it in a `"use client"` file — Prisma will fail at build time because the query engine cannot bundle for the browser.

Connection pooling in serverless — the production gotcha

The single most common production failure with Next.js + Prisma + PostgreSQL is connection exhaustion. Each Vercel serverless function invocation opens a new database connection. A spike of 100 concurrent requests can open 100 Postgres connections — most managed Postgres tiers cap at 20–60. Two production-grade fixes exist. First, use a connection pooler in front of Postgres: Supabase ships PgBouncer on port 6543 with their pooler URL, Neon offers an integrated pooler at the `-pooler` hostname, and self-hosted Postgres should run PgBouncer in transaction mode. Second, use Prisma Accelerate or Prisma Data Proxy — these proxy queries through Prisma's connection pool, so the function only opens an HTTP connection to Accelerate, not a TCP connection to Postgres. For new projects, Accelerate is the simplest path; for cost-sensitive workloads on Supabase or Neon, the platform pooler is free.

Migrations in CI/CD — the workflow that does not break production

Run `prisma migrate dev` only on your local machine, never in CI or production. It is interactive and will reset the database if it detects schema drift. The production-safe command is `prisma migrate deploy`, which applies pending migrations transactionally without prompts. Wire it into your deploy pipeline so migrations run before the new application code goes live. The migration files in `prisma/migrations/` are committed to git — review them in pull requests the same way you review code, because a careless ALTER TABLE can lock a production table for minutes. For destructive migrations (dropping columns, renaming with data movement), prefer the expand-and-contract pattern: ship a migration that adds the new shape, deploy code that reads from both old and new, backfill data, then ship a follow-up migration that drops the old shape. This sequence avoids the deploy-window race condition where in-flight requests fail against a half-migrated schema.

Edge runtime — what works and what does not

Next.js Server Components and Route Handlers can opt into the Edge runtime by exporting `export const runtime = 'edge'`. The standard Prisma client cannot run on Edge — the query engine is a native binary, and Edge runtimes ship a stripped-down V8 with no Node APIs. You have three options. First, run those routes on the Node runtime (the default) and skip Edge entirely — fine for most SaaS, since the latency gain from Edge is often noise next to the database round trip anyway. Second, use Prisma Accelerate, which makes the client edge-compatible by proxying queries over HTTP. Third, use the `@prisma/adapter-pg` driver adapter with a fetch-based Postgres client like Neon's serverless driver — this is the most efficient setup for edge-heavy workloads but requires more configuration. Most projects should default to Node runtime until they have a measured latency reason to move.

Avoiding N+1 — `select` and `include` patterns

The classic ORM trap is loading a list of records, then iterating and querying for related data per record — 1 query becomes N+1 queries. Prisma handles this with explicit `select` and `include` arguments. To fetch posts with their authors in one query: `prisma.post.findMany({ include: { author: true } })`. To fetch only specific fields and reduce payload size: `prisma.post.findMany({ select: { id: true, title: true, author: { select: { name: true } } } })`. The `include` and `select` arguments are typed end-to-end, so the returned type narrows to exactly the fields you requested. For nested relations, use the same pattern recursively. When in doubt, log queries with `new PrismaClient({ log: ['query'] })` in development — if a single page render is firing more than 3–5 SQL queries, an N+1 is hiding in your render tree.

Type safety at the API boundary

Prisma generates types from your schema, but those types live server-side. To share them with client components or external API consumers, do not export Prisma's internal types directly — they leak query-shape implementation details. Instead, define explicit DTO types in a shared module: `type PublicPost = { id: string; title: string; authorName: string }`. Server-side queries return Prisma types; map them to DTOs at the boundary. This decouples your wire format from your database schema, so future migrations cannot accidentally break consumers. For tRPC or Next.js Route Handlers with Zod, define a Zod schema for the response, infer the TypeScript type from it, and validate the Prisma-shaped result before sending. The Prisma docs call this pattern `model-to-DTO mapping` and it is the single highest-leverage discipline for long-term maintainability.

Common production pitfalls

Three failure modes hit production Prisma + PostgreSQL + Next.js stacks repeatedly. First, forgetting to set `directUrl` alongside `url` when using a pooler — migrations fail because PgBouncer in transaction mode cannot run DDL. The fix: point `url` at the pooled connection (port 6543 on Supabase) and `directUrl` at the direct connection (port 5432) in `schema.prisma`. Second, large transactions timing out because Prisma's default transaction timeout is 5 seconds — pass `{ timeout: 30000 }` to `$transaction` for any operation that touches more than a few hundred rows. Third, accidentally running `prisma migrate reset` in production via a misconfigured environment variable — gate destructive Prisma commands behind a `if (process.env.NODE_ENV === 'production') process.exit(1)` check in any script that runs them, or use `prisma migrate deploy` exclusively in CI.

Other integration guidesView all →
Related

Need this built?