The Prisma Client Singleton Pattern in Next.js
Step 7 of 31 — Next.js Tutorial Series | Source code for this step
What You Will Build
By the end of this step, you will have created a Prisma Client singleton — a shared database connection that your entire Next.js application uses. This is the bridge between your Next.js code and the Prisma Postgres database.
Goal: Create lib/prisma.ts and use it in a Server Component to verify the database connection with a real query.
Table of Contents
- The Problem: Multiple Database Connections
- Core Concept: The Singleton Pattern
- Core Concept: PrismaPg Adapter
- Create the Prisma Client Singleton
- Use It in a Server Component
- Verify the Connection
- Summary & Key Takeaways
The Problem: Multiple Database Connections
Look at the prisma/seed.ts file from Step 6:
import { PrismaClient } from '../app/generated/prisma/client'
import { PrismaPg } from '@prisma/adapter-pg'
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL })
const prisma = new PrismaClient({ adapter })
Every time we create a Prisma Client, it opens a new database connection. In the seed script, this is fine — we run it once. But your Next.js app runs on a long-lived server where pages and API routes might be called thousands of times per second.
If every route created its own Prisma Client, we would have:
- Connection pool exhaustion — too many connections, server crashes
- Performance degradation — establishing new connections is slow
- Resource waste — each connection consumes memory
The solution: Create one shared Prisma Client instance and reuse it everywhere.
Core Concept: The Singleton Pattern
A singleton is a pattern where only one instance of something exists in your application. You create it once, and then every file imports and uses that same instance.
Application
├── lib/prisma.ts ← Create Prisma Client once here
├── app/page.tsx ← Import and use it here
├── app/posts/page.tsx ← Import and use it here
└── app/api/posts/route.ts ← Import and use it here
All three routes query through the same Prisma Client instance, reusing the same database connection pool.
Core Concept: PrismaPg Adapter
In the seed script, we used PrismaPg — the PostgreSQL adapter. Let's review what it does:
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL })
const prisma = new PrismaClient({ adapter })
The adapter is a bridge between:
- Prisma's query engine (the TypeScript interface you write)
- PostgreSQL's wire protocol (the network protocol that speaks to the database)
Without the adapter, Prisma Client wouldn't know how to talk to PostgreSQL.
Why is the adapter needed? Prisma is database-agnostic — it supports PostgreSQL, MySQL, SQLite, etc. The adapter you use determines which database Prisma connects to. For Prisma Postgres (our cloud-hosted database), we use
PrismaPg.
Create the Prisma Client Singleton
Create the file lib/prisma.ts in your project root:
import { PrismaClient } from '../app/generated/prisma/client'
import { PrismaPg } from '@prisma/adapter-pg'
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL })
const prisma = new PrismaClient({ adapter })
const globalForPrisma = global as unknown as { prisma: typeof prisma }
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
export default prisma
Let's break this down:
Import the Prisma Client
import { PrismaClient } from '../app/generated/prisma/client'
import { PrismaPg } from '@prisma/adapter-pg'
PrismaClient— auto-generated from your schema (Step 5)PrismaPg— the PostgreSQL adapter from the package we installed in Step 4
Create the Adapter and Client
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL })
const prisma = new PrismaClient({ adapter })
Same as in the seed script — create an adapter with the database URL, then pass it to Prisma Client.
The Singleton Logic
const globalForPrisma = global as unknown as { prisma: typeof prisma }
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
This is the key part. Here's why it exists:
During development, Next.js hot-reloads your code — every file change re-imports your modules. If we created new PrismaClient() directly, every hot-reload would create a new Prisma Client, exhausting connections.
By storing it on the global object, we ensure that even if lib/prisma.ts is re-imported, the same Prisma Client instance is reused across hot-reloads.
In production, we don't need this because the server doesn't hot-reload.
Export the Singleton
export default prisma
Now anywhere in your app, you can import this shared instance:
import prisma from '@/lib/prisma'
const users = await prisma.user.findMany()
Use It in a Server Component
Open your existing app/page.tsx. You do not need to replace anything — just make three small additions:
1. Add the import at the top:
import prisma from '@/lib/prisma'
2. Make the function async and add the two count queries:
export default async function Home() {
const postCount = await prisma.post.count()
const userCount = await prisma.user.count()
// ...rest of your return stays the same
}
3. Add a stats display block inside your return, between the intro paragraph and the existing cards grid:
{/* Live database stats */}
<div className="mb-8 flex gap-4">
<div className="rounded-lg bg-blue-50 px-6 py-4 text-center">
<p className="text-3xl font-bold text-blue-600">{postCount}</p>
<p className="text-sm text-blue-700">Posts</p>
</div>
<div className="rounded-lg bg-green-50 px-6 py-4 text-center">
<p className="text-3xl font-bold text-green-600">{userCount}</p>
<p className="text-sm text-green-700">Users</p>
</div>
</div>
Your full app/page.tsx should look like this:
import prisma from '@/lib/prisma'
export default async function Home() {
const postCount = await prisma.post.count()
const userCount = await prisma.user.count()
return (
<>
<h2 className="mb-4 text-4xl font-bold text-gray-900">
Welcome to Superblog
</h2>
<p className="mb-8 text-lg text-gray-600">
A full-stack blog application built with Next.js, Prisma, and
NextAuth.js. We are building this step by step.
</p>
{/* Live database stats */}
<div className="mb-8 flex gap-4">
<div className="rounded-lg bg-blue-50 px-6 py-4 text-center">
<p className="text-3xl font-bold text-blue-600">{postCount}</p>
<p className="text-sm text-blue-700">Posts</p>
</div>
<div className="rounded-lg bg-green-50 px-6 py-4 text-center">
<p className="text-3xl font-bold text-green-600">{userCount}</p>
<p className="text-sm text-green-700">Users</p>
</div>
</div>
<div className="grid gap-6 md:grid-cols-2">
<div className="p-6 bg-white rounded-lg shadow-md">
<h3 className="mb-2 text-lg font-semibold text-gray-800">
📝 Blog Posts
</h3>
<p className="text-gray-600">
Create, read, and delete blog posts. Coming once we connect to the
database.
</p>
</div>
<div className="p-6 bg-white rounded-lg shadow-md">
<h3 className="mb-2 text-lg font-semibold text-gray-800">
🔐 Authentication
</h3>
<p className="text-gray-600">
User registration and login with NextAuth.js. Coming in a later
step.
</p>
</div>
</div>
</>
)
}
Important: This is an async Server Component:
- The component is defined with
async - We can use
awaitdirectly in the component body - The queries run on the server before the HTML is sent to the browser
Verify the Connection
Run your development server:
pnpm dev
Open http://localhost:3000 in your browser. You should see your styled home page with two colored stat boxes showing:
- 14 Posts (blue)
- 5 Users (green)
These are the counts from your seeded data (Step 6). The rest of your page layout stays exactly as it was.
Getting an error like "Cannot find module"? Make sure:
npx prisma generatewas run (Step 6)lib/prisma.tsis at the correct pathapp/generated/prisma/clientexists
If the counts are correct, your Next.js app is successfully querying the database.
Summary & Key Takeaways
| Concept | What it means |
|---|---|
| Singleton | A single shared instance of something, reused everywhere in the app |
| Prisma Client singleton | A shared database connection pool for your entire application |
PrismaPg adapter | The bridge between Prisma and Prisma Postgres |
| Async Server Component | A React component marked with async that can await database queries before rendering |
| Hot-reload safety | Storing the client on the global object prevents hot-reloads from exhausting the connection pool |
What is Next
In Step 8, we will render actual blog posts from the database. Instead of hardcoded placeholder posts, we will use prisma.post.findMany() to fetch real posts and display them on the page.