Overview

@prisma/client is the generated, 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. This page covers client instantiation, lifecycle management, logging, and middleware.

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.
  • Do not commit the generated node_modules/.prisma/client to source control. The .gitignore should exclude it.
  • If the output path is custom (set via generator client { output = "..." }), adjust postinstall accordingly. 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 "@prisma/client"
 
const prisma = new PrismaClient()
 
export default prisma

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 "@prisma/client"
 
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient }
 
export const prisma = globalForPrisma.prisma ?? new PrismaClient()
 
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({
  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 middleware for cross-cutting concerns

$use adds middleware that runs around every query. Use it for soft-delete filters, audit logging, and multi-tenant row isolation.

prisma.$use(async (params, next) => {
  if (params.model === "Post" && params.action === "findMany") {
    params.args.where = { ...params.args.where, deletedAt: null }
  }
  return next(params)
})
  • Middleware stacks in insertion order. Put tenant isolation middleware first.
  • Do not use middleware for caching. It intercepts every query, making cache invalidation unpredictable.
  • Prisma’s middleware API will eventually be replaced by query extensions (prisma.$extends). Prefer extensions for new code. The pattern is the same; the API is stable.

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 "@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.