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.

DatabasePackageClass
Postgres, Supabase, direct Neon@prisma/adapter-pgPrismaPg
Neon over WebSocket or HTTP@prisma/adapter-neonPrismaNeon, PrismaNeonHttp
SQLite file@prisma/adapter-better-sqlite3PrismaBetterSqlite3
libSQL, Turso@prisma/adapter-libsqlPrismaLibSql
Cloudflare D1@prisma/adapter-d1PrismaD1, PrismaD1Http
MariaDB, MySQL@prisma/adapter-mariadbPrismaMariaDb
PlanetScale@prisma/adapter-planetscalePrismaPlanetScale
SQL Server@prisma/adapter-mssqlPrismaMssql
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 = prisma

In 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` },
})
  • query rewrites or wraps operations; call query(args) exactly once. A soft-delete filter on findMany does not cover findFirst, count, or aggregate; hook each operation you rely on, or use $allOperations.
  • Do not cache inside a query hook; it fires on every matching call, so invalidation becomes unpredictable.
  • result fields are virtual. needs lists the columns Prisma selects for you, and you cannot filter on computed fields in where. Promote the value to a column if you need to query it.
  • model and client add methods for domain operations and helpers. See prisma-raw-queries for raw maintenance queries.
  • The tx client inside an interactive $transaction carries 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.