Overview
The generated Prisma client is the typed query builder emitted from schema.prisma: one property per model, with return types inferred from the query shape. In Prisma 7 the client has no built-in engine, so you construct it with a driver adapter and import it from the generator’s output directory. Construct the client once as a singleton, run prisma generate explicitly after schema changes, log queries through events, and use $extends for cross-cutting behavior.
Run prisma generate explicitly after schema changes
The client is a build artifact. Prisma 7 no longer runs generate after migrate dev or db push.
{ "scripts": { "postinstall": "prisma generate" } }The prisma-client generator writes to the output directory you set, not node_modules. Add that directory to .gitignore and import from it, for example ./generated/prisma/client. See prisma-schema for the generator block.
Pass a driver adapter that matches the database
new PrismaClient() without an adapter fails in Prisma 7. The adapter wraps the Node driver, and the driver owns the connection pool. The only exception is Prisma Accelerate; see prisma-pooling.
| Database | Package | Class |
|---|---|---|
| Postgres, Supabase, direct Neon | @prisma/adapter-pg | PrismaPg |
| Neon over WebSocket or HTTP | @prisma/adapter-neon | PrismaNeon, PrismaNeonHttp |
| SQLite file | @prisma/adapter-better-sqlite3 | PrismaBetterSqlite3 |
| libSQL, Turso | @prisma/adapter-libsql | PrismaLibSql |
| Cloudflare D1 | @prisma/adapter-d1 | PrismaD1, PrismaD1Http |
| MariaDB, MySQL | @prisma/adapter-mariadb | PrismaMariaDb |
| PlanetScale | @prisma/adapter-planetscale | PrismaPlanetScale |
| SQL Server | @prisma/adapter-mssql | PrismaMssql |
import { PrismaPg } from "@prisma/adapter-pg"
import { PrismaClient } from "../generated/prisma/client"
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL })
export const prisma = new PrismaClient({ adapter })SQLite adapters take { url }: new PrismaLibSql({ url }) or new PrismaBetterSqlite3({ url }). The underlying driver (pg, @libsql/client, better-sqlite3) installs as a dependency of the adapter. Keep the adapter package on the same major version as prisma. Build the adapter and client once, never per request.
Use a singleton, and survive hot reload in development
Multiple PrismaClient instances mean multiple pools. Export one instance from one module and import it everywhere. Dev servers (Next.js, Vite) reload modules and would create a client per reload, so cache it on globalThis outside production.
const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient }
export const prisma =
globalForPrisma.prisma ??
new PrismaClient({ adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL }) })
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prismaIn serverless handlers, create the client at module scope so warm invocations reuse it, and do not call $disconnect per invocation. Pool limits and poolers are in prisma-pooling.
Log queries through events, not stdout
const prisma = new PrismaClient({
adapter,
log: [
{ emit: "event", level: "query" },
{ emit: "stdout", level: "warn" },
{ emit: "stdout", level: "error" },
],
})
prisma.$on("query", (e) => console.log(`${e.query} (${e.duration}ms)`))Route query events to your telemetry pipeline instead of stdout, and turn query logging off in production unless you are debugging: the emitted parameters can contain personal data. Pair slow queries with postgres-explain.
Use $extends for cross-cutting behavior
$extends returns a new client with extra behavior; the base client is unchanged. Export and use the extended client everywhere. The $use middleware API was deprecated in 4.16.0 and is removed in Prisma 7. Chain $extends calls to compose.
Four components exist: query, result, model, and client.
const prisma = base.$extends({
query: {
post: {
findMany({ args, query }) {
args.where = { ...args.where, deletedAt: null }
return query(args)
},
},
},
result: {
user: {
fullName: {
needs: { firstName: true, lastName: true },
compute: (u) => `${u.firstName} ${u.lastName}`,
},
},
},
model: {
user: { signUp: (email: string) => base.user.create({ data: { email } }) },
},
client: { $health: () => base.$queryRaw`SELECT 1` },
})queryrewrites or wraps operations; callquery(args)exactly once. A soft-delete filter onfindManydoes not coverfindFirst,count, oraggregate; hook each operation you rely on, or use$allOperations.- Do not cache inside a
queryhook; it fires on every matching call, so invalidation becomes unpredictable. resultfields are virtual.needslists the columns Prisma selects for you, and you cannot filter on computed fields inwhere. Promote the value to a column if you need to query it.modelandclientadd methods for domain operations and helpers. See prisma-raw-queries for raw maintenance queries.- The
txclient inside an interactive$transactioncarries the same extensions; see prisma-transactions.
Derive types from the generated client
Use the generated payload types instead of hand-written interfaces.
import { Prisma } from "./generated/prisma/client"
type UserWithOrders = Prisma.UserGetPayload<{ include: { orders: true } }>Avoid any casts on query results; see typescript.