How to Add Prisma to Next.js (2026 Guide)
Prisma is a schema-first ORM: you describe your tables in one file, it generates a fully typed client, and it manages the SQL migrations that keep the database in step. Version 7 changed enough that older tutorials are now wrong — the Rust engine is gone, driver adapters are mandatory, the generated client no longer lives in node_modules, and connection URLs moved out of the schema. This guide is the current path, end to end.
What Prisma is, and when it is the right call
Prisma has three parts that people often conflate. Prisma Schema is the declarative file (`prisma/schema.prisma`) where you model tables, columns, relations and indexes. Prisma Migrate turns diffs in that file into ordinary, reviewable SQL migration files. Prisma Client is the generated, fully typed query API you actually import — `prisma.user.findMany(...)` and friends. You can use the schema and the client without ever running Migrate (point it at an existing database with `prisma db pull`), but the three together are the reason people pick it.
Pick Prisma when the schema is the centre of gravity: a product with a dozen or more related tables, a team that wants migrations reviewed in pull requests, and developers who would rather read a schema file than a folder of SQL. The typed client is genuinely typed — rename a column in the schema, regenerate, and every query that referenced the old name fails to compile rather than failing at runtime.
Skip Prisma when you need hand-tuned SQL as the normal case (recursive CTEs, window functions, heavy analytical queries), when your bundle budget is measured in single-digit kilobytes, or when you want the query builder to be a thin, zero-abstraction layer over SQL — that is Drizzle's pitch, and it is a fair one. If you have not settled what the app even is yet, draft a structured build prompt first: the entities and screens you list there are the models you are about to write.
Step 1 — Install and initialise
Two packages plus a driver adapter. `npm i @prisma/client @prisma/adapter-pg pg` for a plain Postgres connection, and `npm i -D prisma tsx dotenv`. On Neon use `@prisma/adapter-neon` instead of `@prisma/adapter-pg`; on PlanetScale or Vercel Postgres, use that vendor's adapter. The adapter is not optional in version 7 — the client has no built-in database driver any more.
Run `npx prisma init --datasource-provider postgresql`. That writes `prisma/schema.prisma` and, since version 7, a `prisma.config.ts` at the project root. Put your connection string in `.env` as `DATABASE_URL` and add the same variable to your hosting provider's environment settings.
`prisma.config.ts` is where the CLI now reads the database URL from, because the `url` field in the schema's `datasource` block is no longer supported. A minimal working config: `import "dotenv/config"; import { defineConfig } from "prisma/config"; export default defineConfig({ schema: "prisma/schema.prisma", migrations: { path: "prisma/migrations" }, datasource: { url: process.env.DATABASE_URL } })`. Note the explicit `dotenv/config` import — version 7 stopped loading `.env` files for you.
One more thing that trips people up on the first deploy: the generated client is not committed and is not in `node_modules`, so it has to be regenerated on every build. Set `"build": "prisma generate && next build"` in `package.json` and, if your installer runs it, `"postinstall": "prisma generate"` as well.
Step 2 — Model the schema
The generator block is the part that changed. Use `generator client { provider = "prisma-client" output = "../src/generated/prisma" }` — note `prisma-client`, not the older `prisma-client-js`, and note that `output` is now required. The client is written to that folder in your own source tree, which means your imports look like `import { PrismaClient } from "@/generated/prisma/client"` rather than `from "@prisma/client"`. Add the output folder to `.gitignore`.
The datasource block shrinks to `datasource db { provider = "postgresql" }`. No `url`, no `directUrl` — those live in `prisma.config.ts` for the CLI and in the adapter for the runtime.
Models are the familiar part. `model Project { id String @id @default(cuid()) name String slug String @unique ownerId String owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([ownerId, createdAt]) }`. Two habits worth forming early: put an explicit `@@index` on every column combination you filter or sort by, and set `onDelete` on every relation rather than discovering the default when a delete fails at 2am.
A `@unique` string column is the usual home for a human-readable URL segment. Generate it from the title at write time and store it — do not derive it on read, or the URL silently changes when someone edits the title. If you want to see what a given title collapses to, run it through a slug generator before you pick the normalisation rules you will encode.
Step 3 — The client singleton
Next.js hot-reloads modules in development, and a naive `new PrismaClient()` at module scope creates a fresh client — and a fresh connection pool — on every reload until the database refuses new connections. The fix is the standard global-cache singleton, and with version 7 it also carries the adapter.
`src/lib/db.ts`: `import { PrismaClient } from "@/generated/prisma/client"; import { PrismaPg } from "@prisma/adapter-pg"; const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient }; function createPrismaClient() { const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL! }); return new PrismaClient({ adapter }) } export const prisma = globalForPrisma.prisma ?? createPrismaClient(); if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma`.
This file is server-only. Importing it from a Client Component pulls a database driver into the browser bundle, which fails loudly if you are lucky and leaks your connection string if you are not. Add `import "server-only"` at the top of the file so the mistake becomes a build error rather than a code review question.
Step 4 — Read in Server Components, write in Server Actions
App Router pages are Server Components by default, so a read is just an awaited call in the component body: `const projects = await prisma.project.findMany({ where: { ownerId: user.id }, orderBy: { createdAt: "desc" }, select: { id: true, name: true, slug: true } })`. No API route, no fetch, no serialization boundary in the middle. Reach for `select` rather than returning whole rows — it keeps the payload small and stops a future column addition from silently widening every query.
Writes belong in Server Actions. A function marked `"use server"` that validates its input, calls `prisma.project.create({ data: { ... } })`, then calls `revalidatePath("/projects")` so the Server Component read above re-runs. Validate with Zod at the top of the action and treat everything arriving from the client as hostile — a Server Action is a public HTTP endpoint with a nicer calling convention, not a private function.
Relations are where the N+1 problem shows up. `include: { owner: true }` on a `findMany` issues a bounded number of queries; a `for` loop that calls `findUnique` per row issues one per row. When you need rows by a set of ids, use `where: { id: { in: ids } }` once. When you need a genuinely awkward aggregate, drop to `prisma.$queryRaw` with a tagged template — it is parameterised, so it is safe, and it is a normal thing to do rather than an admission of defeat.
Transactions come in two shapes. `prisma.$transaction([a, b, c])` runs an array of independent queries atomically. `prisma.$transaction(async (tx) => { ... })` gives you an interactive transaction where later statements depend on earlier results — use it for read-modify-write sequences, and keep the body short, because the connection is held for the duration. If you would rather have this shape generated than typed out, describe the app and InBuild generates the Next.js code — a paid plan is required to generate.
Step 5 — Migrations
In development, `npx prisma migrate dev --name add_projects` diffs the schema against the database, writes a timestamped SQL file under `prisma/migrations/`, applies it, and regenerates the client. Read the generated SQL before you commit it. Renaming a column is the classic trap: Prisma cannot tell a rename from a drop-plus-add, so it will happily generate a destructive pair unless you edit the migration by hand.
In production, `npx prisma migrate deploy` applies pending migrations and nothing else — it never generates, never resets, never prompts. Run it as a release step, not inside the app build, so that a rollback of the deploy does not leave you with a half-applied schema and no way to reason about which version of the code is talking to it.
`prisma db push` skips the migration history entirely and force-syncs the database to the schema. It is genuinely useful for a throwaway local database or an early prototype where the schema changes hourly. Using it against a database with real data is how people lose columns.
Additive migrations deploy cleanly; destructive ones need two releases. To remove a column: ship one release that stops reading and writing it, then ship a second that drops it. To rename: add the new column, backfill, switch the code, drop the old one. The intermediate state is ugly for a day and saves an outage.
Connection pooling on serverless
Every serverless invocation that opens its own TCP connection to Postgres is competing for a pool that is usually capped somewhere in the low hundreds. Under a traffic spike the connections, not the CPU, are what fall over. There are three real answers and you should pick one deliberately.
Use a pooler in front of the database. Supabase exposes port 6543 for its transaction-mode pooler alongside 5432 for direct connections; point the runtime at 6543 and the migration URL at 5432, because transaction-mode poolers do not support the session-level statements migrations need. A self-managed PgBouncer in transaction mode behaves the same way.
Use an HTTP-based driver. Neon's serverless driver talks to Postgres over HTTP, so there is no long-lived socket to exhaust; `@prisma/adapter-neon` is the adapter for it. This is the lowest-friction option on Vercel and the one this site runs.
Or put Prisma's own connection pooler in front. That is a hosted service with its own pricing, so treat it as a decision rather than a default. Whichever you choose, check the pool size your platform actually gets — the failure mode is a burst of `Timed out fetching a new connection from the connection pool` errors that never reproduce locally.
Common mistakes
Following a version 6 tutorial. If a guide tells you to write `url = env("DATABASE_URL")` inside the `datasource` block, import from `@prisma/client`, or use `provider = "prisma-client-js"`, it predates version 7 and will not run. The error messages are unhelpful because the schema parses fine and fails later.
Forgetting `prisma generate` in CI. Works locally, fails on the build server with a module-not-found on the generated path, because the generated client is in `.gitignore` and no longer in `node_modules`.
Passing non-serializable values across the RSC boundary. `BigInt` columns and Prisma's `Decimal` instances are class instances, not JSON, and handing them to a Client Component throws. Map them to `string` or `number` in the Server Component before you pass them down.
Reaching for middleware. `prisma.$use()` is gone. The replacement is Client Extensions — `prisma.$extends({ query: { project: { async findMany({ args, query }) { ... } } } })` — which is typed, composable, and scoped to the models you name instead of intercepting everything.
Treating `@@index` as a later optimisation. Prisma will not warn you that a `where` clause has no index behind it. The query is instant on the hundred rows you seeded and catastrophic on the hundred thousand rows you have in six months. Add the index in the same pull request as the query.
Running the connection pool at the wrong layer. If you have already got a pooler in front of Postgres, a large client-side pool on top of it multiplies rather than helps. One pool, one place.
How to do it
- 1
Install the packages
npm i @prisma/client @prisma/adapter-pg pg and npm i -D prisma tsx dotenv. On Neon, use @prisma/adapter-neon instead. A driver adapter is mandatory in Prisma 7.
- 2
Initialise
npx prisma init --datasource-provider postgresql writes prisma/schema.prisma and prisma.config.ts. Put DATABASE_URL in .env and in your host's environment settings.
- 3
Configure prisma.config.ts
defineConfig from prisma/config, with schema, migrations.path, and datasource.url read from process.env. Import dotenv/config explicitly — v7 no longer loads .env for you.
- 4
Write the schema
generator client { provider = "prisma-client" output = "../src/generated/prisma" }, datasource db { provider = "postgresql" } with no url, then your models with explicit @@index and onDelete.
- 5
Create the client singleton
src/lib/db.ts caches a PrismaClient built with the adapter on globalThis outside production, so hot reload does not open a new pool per reload. Add import "server-only" at the top.
- 6
Run the first migration
npx prisma migrate dev --name init locally. Read the generated SQL before committing. Use npx prisma migrate deploy as a release step in production.
- 7
Wire the build
Set "build": "prisma generate && next build". The generated client is gitignored and not in node_modules, so CI must regenerate it.
Frequently asked questions
Why does my import from @prisma/client suddenly fail?
Because Prisma 7's prisma-client generator writes the client into your own source tree at the path you set as output, not into node_modules. Import from that path instead — for example @/generated/prisma/client if your output is ../src/generated/prisma. The @prisma/client package is still installed; it just no longer holds the generated types.
Do I really need a driver adapter?
Yes, in version 7. The Rust query engine that used to ship its own database driver is gone, replaced by a TypeScript and WebAssembly engine, so the client needs a Node driver handed to it. Use @prisma/adapter-pg with the pg package for a normal Postgres connection, @prisma/adapter-neon for Neon, or the adapter your database vendor publishes.
Prisma or Drizzle?
Prisma if you want a declarative schema file, generated migrations, and a query API that reads like an object graph. Drizzle if you want your schema defined in TypeScript, SQL-shaped queries, and a smaller runtime. Both are fully typed and both work on the App Router. The honest tiebreaker is the team: Prisma's schema file is easier for a mixed-experience team to review, Drizzle is easier for people who already think in SQL.
Can I use Prisma in a Client Component or on the Edge runtime?
Never in a Client Component — it is a database driver and belongs on the server. On the Edge runtime it depends on the adapter: HTTP-based drivers such as Neon's work there, while adapters built on TCP sockets do not. Check the adapter's own documentation before you set the runtime, and default to the Node runtime for database routes unless you have a reason not to.
How do I seed the database?
Write a prisma/seed.ts that imports your client singleton and upserts the rows you need, then run it with tsx. Prefer upsert over create so the script is idempotent and can be run repeatedly against a development database. Keep seeds for reference data and local fixtures; do not use them to patch production data, because there is no record of what ran.
What happened to prisma.$use middleware?
It was removed. The replacement is Client Extensions via prisma.$extends, which lets you wrap queries, add computed fields to result types, or define custom model methods. Extensions are typed and scoped to the models and operations you name, so a soft-delete filter applies to the models that have a deletedAt column rather than intercepting every query in the application.
Free tools used in this guide
No signup — each runs in the browser or as a single server-side fetch.
Ready to build?
See a real InBuild template rendered live, with the exact exported code, before you pick a plan — no account needed.
Or skip the tutorial: describe it and InBuild generates the Next.js code for you. A paid plan is required to generate.
Build this with InBuildMore Next.js guides
How to Add Stripe to a Next.js Site (2026 Step-by-Step)
A complete walkthrough of wiring Stripe Checkout to a Next.js App Router project — products, prices, webhooks, customer portal, and the gotchas.
ReadTutorialHow to Add Authentication to Next.js (2026 Step-by-Step)
A complete walkthrough of adding auth to a Next.js App Router site — comparing Auth.js, Clerk, and roll-your-own JWT — with the tradeoffs and a working example.
ReadTutorialNext.js SEO: The Complete Setup Guide (2026)
Everything you need to set up SEO correctly on a Next.js App Router site: metadata, sitemap, robots.txt, structured data, OG images, Core Web Vitals — and whether you still need next-seo.
Read