The Prisma Client Singleton Pattern in Next.js

Step 7 of 31Next.js Tutorial Series | Source code for this step

Live Demo →


What You Will Build

↑ Index

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

  1. The Problem: Multiple Database Connections
  2. Core Concept: The Singleton Pattern
  3. Core Concept: PrismaPg Adapter
  4. Create the Prisma Client Singleton
  5. Use It in a Server Component
  6. Verify the Connection
  7. Summary & Key Takeaways

The Problem: Multiple Database Connections

↑ Index

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

↑ Index

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

↑ Index

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

↑ Index

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

↑ Index

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 await directly in the component body
  • The queries run on the server before the HTML is sent to the browser

Verify the Connection

↑ Index

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 generate was run (Step 6)
  • lib/prisma.ts is at the correct path
  • app/generated/prisma/client exists

If the counts are correct, your Next.js app is successfully querying the database.


Summary & Key Takeaways

↑ Index

ConceptWhat it means
SingletonA single shared instance of something, reused everywhere in the app
Prisma Client singletonA shared database connection pool for your entire application
PrismaPg adapterThe bridge between Prisma and Prisma Postgres
Async Server ComponentA React component marked with async that can await database queries before rendering
Hot-reload safetyStoring 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.