Overview

The generated Prisma client is the fully typed query builder that Prisma emits from your schema.prisma. The client exposes one method per model (prisma.user, prisma.order, etc.) and infers return types from the query shape at compile time. You never write a DTO by hand for a standard query. In Prisma 7 you import PrismaClient from the generator’s output path and construct it with a driver adapter. This page covers client instantiation, lifecycle management, logging, and client extensions.

Run prisma generate after every schema change

The generated client is a build artifact. It must stay in sync with schema.prisma.

{
  "scripts": {
    "postinstall": "prisma generate"
  }
}
  • Wire prisma generate into postinstall so CI and fresh npm install always emit a current client.
  • The Prisma 7 prisma-client generator writes to a required output directory in your source tree (for example src/generated/prisma), not node_modules. Add that directory to .gitignore; do not commit generated code.
  • Adjust postinstall and your import path to match the output you set. See prisma-schema for generator config.

Use a singleton in long-running servers

In a standard Node server, instantiate PrismaClient once and reuse it for the lifetime of the process. Multiple instances create multiple connection pools.

// lib/prisma.ts
import { PrismaClient } from "../generated/prisma/client"
import { PrismaPg } from "@prisma/adapter-pg"
 
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL })
const prisma = new PrismaClient({ adapter })
 
export default prisma
  • In Prisma 7 (Rust engine removed), new PrismaClient() without a driver adapter throws. Pass the adapter for your database: @prisma/adapter-pg for postgres, @prisma/adapter-libsql for SQLite or Turso. See prisma-driver-adapters.
  • Import prisma from this module everywhere. Never call new PrismaClient() inside a request handler or model function.

Prevent hot-reload exhaustion in development

Next.js and Vite dev servers hot-reload modules on save, which creates a new PrismaClient instance on every reload. The old instance’s connection pool stays open. Under a few minutes of active development you exhaust the Postgres connection limit.

// lib/prisma.ts
import { PrismaClient } from "../generated/prisma/client"
import { PrismaPg } from "@prisma/adapter-pg"
 
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient }
 
const makeClient = () =>
  new PrismaClient({ adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL }) })
 
export const prisma = globalForPrisma.prisma ?? makeClient()
 
if (process.env.NODE_ENV !== "production") {
  globalForPrisma.prisma = prisma
}
  • Stash the instance on globalThis, which survives HMR.
  • Never use the globalThis trick in production. A real production process does not hot-reload.

Call $connect and $disconnect explicitly in Lambda

In serverless environments, each invocation may get a cold container. The client connects lazily by default, but long-running Lambda handlers benefit from explicit lifecycle control.

export const handler = async (event: APIGatewayEvent) => {
  await prisma.$connect()
  try {
    const result = await prisma.user.findUnique({ where: { id: event.pathParameters?.id } })
    return { statusCode: 200, body: JSON.stringify(result) }
  } finally {
    await prisma.$disconnect()
  }
}
  • Use $connect before the first query when you want to fail fast on connection errors.
  • Always $disconnect in finally so the container does not hold a connection across idle time.
  • For high-throughput Lambdas, use a pooler instead. See prisma-pooling.

Configure logging for query visibility

Prisma can log queries, query parameters, warnings, and errors. Enable selectively; logging every query in production generates noise and leaks parameter values to stdout.

const prisma = new PrismaClient({
  adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL }),
  log: [
    { emit: "event", level: "query" },
    { emit: "stdout", level: "error" },
    { emit: "stdout", level: "warn" },
  ],
})
 
prisma.$on("query", (e) => {
  console.log(`${e.query} (${e.duration}ms)`)
})
  • Use emit: "event" to route query logs to your observability pipeline rather than stdout.
  • Log query duration to identify slow paths. Pair with postgres-explain to diagnose them.
  • Disable "query" logs in production unless you are actively debugging. The emitted parameters may contain PII.

Use client extensions for cross-cutting concerns

$extends is the mechanism for soft-delete filters, audit logging, and multi-tenant row isolation. The $use middleware API was deprecated in v4.16.0 and removed in v6.14.0; it does not exist in Prisma 7. Use $extends({ query: { ... } }) to intercept queries.

const prisma = base.$extends({
  query: {
    post: {
      findMany({ args, query }) {
        args.where = { ...args.where, deletedAt: null }
        return query(args)
      },
    },
  },
})
  • $extends returns a new client; assign it and use that instance. The base client is unchanged.
  • Do not use a query extension for caching. It intercepts every query, making cache invalidation unpredictable. Use the result component or a dedicated cache instead.
  • For the full set of components (query, result, model, client), see prisma-client-extensions.

Derive types from the generated client

Prisma generates payload types for every model and query shape. Use them instead of hand-written interfaces.

import { Prisma } from "./generated/prisma/client"
 
type UserWithOrders = Prisma.UserGetPayload<{
  include: { orders: true }
}>
  • GetPayload narrows the return type to exactly the selected fields, including nested includes.
  • Avoid any casts on query results. The generated types are the contract between the schema and the application. See typescript for general type safety rules.