cf.completefrontend
Open lab My progress
THE NEXT.JS FIELD GUIDE

Connecting a database

Connect a Next.js app to Postgres: choose Drizzle or Prisma, see the SQL your queries send, pool connections, keep one client in development, and seed data.

By the end, you can
  • 01
    Choose a database and an ORMCompare Postgres, SQLite and MySQL, a plain driver and an ORM, and Drizzle and Prisma, with the versions current today.
  • 02
    Set up the client wellOpen one client for each server process, size its connection pool, and keep it on globalThis while next dev reloads your files.
  • 03
    Read and seed the dataWrite Drizzle queries, read the SQL they send, and seed the first rows with a script that's safe to run again.

The tea shop's teas move into Postgres

In Fetching data in Server Components, the tea shop's database was one file, and the page wrote its SQL by hand. A real app needs a little more around its database: a description of its tables, one client that every request shares, and a way to put the first rows in. Many apps also use an ORM (object-relational mapper): a library that lets you describe tables and write queries in your own language, and writes the SQL for you. Here the shop keeps its teas in Postgres and reads them with Drizzle ORM.

This example is a real Next.js 16.4.3 project, built with next build and run with next start. Its database is PGlite again: Postgres compiled to WebAssembly, running inside the app, in memory, so the recording needs no database server. The rest is what a project with a Postgres server has: a schema, a client, a seed, and a data module. Click through the files; the schema opens first.

The tea shop on Postgres, with DrizzleNext.js
app/db/lib/
db/schema.ts
import { integer, pgTable, text } from "drizzle-orm/pg-core"; export const teas = pgTable("teas", {  id: text().primaryKey(),  name: text().notNull(),  price: integer().notNull(),  stock: integer().notNull().default(0),});
Recorded from Next.js 16.4.3
localhost:3000/Tea shop
Tea shop
  • Green tea: $4
  • Lemon tea: $3
  • Mint tea: $3

The page as the browser shows it when its HTML arrives, drawn with plain styles. Its JavaScript doesn't run here.

db/schema.ts describes the teas table in TypeScript. Line 3 names it with pgTable, and lines 4 to 7 are its columns: an id that's the primary key (no two teas can share one), a name and a price that can't be empty, and a stock that starts at 0.

In db/index.ts, line 6 opens the database, and line 7 wraps it with Drizzle: db is what every query uses, and logger: true prints each query it sends, with its values. The recordings use it to show you the SQL; keep it to development, because the values can hold private data, like a customer's email address. Lines 11 to 13 make the table, and line 14 calls seed, which puts in the first three teas. In lib/teas.ts, the data module from the last lesson, line 9 is the query: three columns of every tea, sorted by name. The page calls getTeas() inside Suspense, as before.

Open the Terminal tab and scroll to the end, where next start's output follows the build's. The server printed Opening the database once, then the seed's insert, then for each of the two visits the SQL that Drizzle wrote for line 9: select "id", "name", "price" from "teas" order by "teas"."name" asc. The build printed Opening the database too, twice in this recording, while it collected the pages: next build loads your server code to look at each page. How many times is a detail of this build, not something to count on.

On the left, teas.ts, the file lib/teas.ts, calls db.select with orderBy name. An arrow labeled query goes to Drizzle, and an arrow labeled SQL goes from Drizzle to Postgres, which holds the teas table. Arrows back are labeled rows, from Postgres to Drizzle, and objects, from Drizzle to lib/teas.ts.teas.tsdb.select().orderBy(name)queryobjectsDrizzleSQLrowsPostgresteas table3 rowsthe same code, for each visit
One call to getTeas(). Drizzle turns the TypeScript query into SQL and sends it to Postgres, then turns the rows that come back into objects. The recordings' Terminal shows the SQL for each visit.
Real-life analogyThe warehouse and its phone lines

The shop keeps its tea in a warehouse. Assistants don't walk over for every order: they fill in an order form the warehouse understands, and phone it through on one of a few lines the shop pays for. Opening a new line takes a while, so the assistants share the ones they have. On opening day, the warehouse got its first boxes, once.

In real life: The warehouse, where every box of tea is kept
In Next.js: Postgres, where the rows are kept
In real life: The order form, which turns "all teas, by name" into the warehouse's own words
In Next.js: Drizzle, which turns your query into SQL
In real life: A few phone lines to the warehouse, shared by every assistant
In Next.js: A connection pool, shared by every request
In real life: The first boxes delivered on opening day
In Next.js: The seed: the first rows
The main idea

Connect once, query often. Each server process opens one client, with a small pool of connections that every request shares. Read and write through an ORM that writes the SQL and passes your values as parameters. Make the tables with the ORM's migration tool, and put the first rows in with a seed that's safe to run twice.

Choosing a database and an ORM

Two choices come first: which database holds the data, and how your code talks to it.

Which database

Each row is one database: what it is, and when it fits.

Four databases
The databaseWhat it isWhen it fits
PostgresA database server, open source, with tables, SQL, and many extensions.Most new full-stack apps. Many hosts run it for you.
SQLiteA whole database in one file, read by a library inside your app.Small apps on one server, prototypes and tests. Hosted versions, like Turso, add a server.
MySQLA database server, open source, as widely hosted as Postgres.Teams and hosts that already run it.
PGlitePostgres compiled to WebAssembly, running inside your app.Tests, demos and this course's recordings. It isn't a server that many app instances share.

Postgres is a common choice for a new Next.js app, and many hosts run it for you: you get a connection string, an address with the user and password in it, like postgres://user:password@host:5432/teashop, and keep it in a server environment variable, DATABASE_URL. Next.js doesn't mind which you pick: a Server Component calls whatever client you install.

How your code talks to it

Each row is one way to send a query: what you write, and how it looks.

A driver or an ORM
The toolWhat you writeA query for one tea
A driver, like pgSQL text, with parameters.pool.query("SELECT name FROM teas WHERE id = $1", [id])
An ORM that reads like SQL, like DrizzleTypeScript that builds the SQL.db.select().from(teas).where(eq(teas.id, id))
An ORM built on models, like PrismaCalls on a model, with options.prisma.tea.findMany({ where: { id } })

A driver sends the SQL you write; the last lesson's db.query worked that way. An ORM lets you describe the tables once and write queries as code. It writes the SQL, passes values as parameters, and, with TypeScript, knows each row's fields, so a misspelled column is an error in your editor instead of a failed query.

Drizzle and Prisma

Drizzle and Prisma are two popular ORMs for TypeScript, and both document how to use them with Next.js. This is where both stand on October 30, 2026, from each project's docs and from npm.

Drizzle ORM

Drizzle describes tables in TypeScript and writes queries that read like SQL. The recordings use drizzle-orm 0.45.4, the latest on npm. Its command-line tool, drizzle-kit (0.31.11), makes and changes the tables from the schema. Drizzle's own docs already install a 1.0 release candidate, with npm i drizzle-orm@rc; check its upgrade notes before you move to it. Each button below is a version of line 9 of lib/teas.ts. Before you click, guess the SQL each one sends.

Playground: which SQL does Drizzle send?
The tea shop, with this version of lib/teas.tsNext.js
app/db/lib/
lib/teas.ts
import "server-only";import { connection } from "next/server";import { asc } from "drizzle-orm";import { db } from "@/db";import { teas } from "@/db/schema"; export async function getTeas() {  await connection();  return db.select({ id: teas.id, name: teas.name, price: teas.price }).from(teas).orderBy(asc(teas.name));}
Recorded from Next.js 16.4.3
$ next build▲ Next.js 16.4.3 (Turbopack)- Cache Components enabled- Partial Prefetching enabled   Creating an optimized production build ...✓ Compiled successfully  Running TypeScript ...  Finished TypeScript ...  Collecting page data ...Opening the databaseQuery: insert into "teas" ("id", "name", "price", "stock") values ($1, $2, $3, $4), ($5, $6, $7, $8), ($9, $10, $11, $12) on conflict do nothing -- params: ["green", "Green tea", 4, 12, "mint", "Mint tea", 3, 0, "lemon", "Lemon tea", 3, 5]Opening the databaseQuery: insert into "teas" ("id", "name", "price", "stock") values ($1, $2, $3, $4), ($5, $6, $7, $8), ($9, $10, $11, $12) on conflict do nothing -- params: ["green", "Green tea", 4, 12, "mint", "Mint tea", 3, 0, "lemon", "Lemon tea", 3, 5]✓ Generating static pages (3/3)  Finalizing page optimization ... Route (app)┌ ◐ /└ ○ /_not-found ○  (Static)             prerendered as static content◐  (Partial Prerender)  prerendered as static HTML with dynamic server-streamed content $ next start▲ Next.js 16.4.3- Local:         http://127.0.0.1:3000- Network:       http://127.0.0.1:3000✓ ReadyOpening the databaseQuery: insert into "teas" ("id", "name", "price", "stock") values ($1, $2, $3, $4), ($5, $6, $7, $8), ($9, $10, $11, $12) on conflict do nothing -- params: ["green", "Green tea", 4, 12, "mint", "Mint tea", 3, 0, "lemon", "Lemon tea", 3, 5]Query: select "id", "name", "price" from "teas" order by "teas"."name" ascQuery: select "id", "name", "price" from "teas" order by "teas"."name" asc
Try it yourself
All teas, by name

Line 9 asks for three columns of every tea, sorted by name. Drizzle sends select "id", "name", "price" from "teas" order by "teas"."name" asc, with no parameters.

Every version was built with next build from Next.js 16.4.3 and visited; the Terminal ends with what next start printed, with the SQL Drizzle sent for each visit. Only lib/teas.ts changes. Reset goes back to the first version.

Drizzle builds one SQL statement from each chain: select picks the columns, from the table, where adds a condition, and orderBy and limit come last. Values like 0 and 2 never go into the SQL text. They travel on their own, as parameters ($1). That prevents SQL injection: a value from outside, like a visitor's search, changing the SQL command itself, because the database never reads a parameter as part of the command. Each version waits for connection() on line 8 for the reason Fetching data in Server Components showed: PGlite reads the clock while it runs, which Next.js stops during a prerender. A query to a database server suspends on its own and doesn't need the line.

Prisma ORM

Prisma describes tables in its own language, in a schema.prisma file, and generates a typed client from it. This is Prisma ORM 7, the current stable release (@prisma/client 7.10.1), written the way Prisma's Next.js guide sets it up. It isn't part of the recordings, so it doesn't run here:

prisma/schema.prisma (Prisma ORM 7, not recorded)Prisma
generator client {
  provider = "prisma-client"
  output   = "../app/generated/prisma"
}

datasource db {
  provider = "postgresql"
}

model Tea {
  id    String @id
  name  String
  price Int
  stock Int    @default(0)

  @@map("teas")
}

Lines 1 to 4 tell prisma generate to write the client into app/generated/prisma. Lines 6 to 8 say the database is Postgres; its address comes from prisma.config.ts. Lines 10 to 17 describe the same teas table as a model, Tea. Line 16, @@map("teas"), names the table: without it, Prisma would look for a table named Tea, not the teas table the Drizzle schema makes. A query reads like this:

The same query as getTeas(), with Prisma (not recorded)TypeScript
const teas = await prisma.tea.findMany({
  select: { id: true, name: true, price: true },
  orderBy: { name: "asc" },
});

findMany on line 1 reads many rows. Line 2 picks the fields, and line 3 sorts by name: the same rows as Drizzle's line 9, written as options instead of SQL-like steps.

Prisma ORM 8 is a release candidate

On October 30, 2026, Prisma ORM 8 is a release candidate with a new way of working: a contract file instead of schema.prisma, a library named @prisma/orm-postgres instead of @prisma/client, and queries like db.orm.public.User. npm's latest prisma command-line tool is already 8.0.0-rc.28, so npm install prisma installs it. Prisma's docs say to install prisma@7 and @prisma/client@7 if you follow the Prisma 7 setup.

Each row asks the same question of both, as their docs answer it on October 30, 2026.

Drizzle and Prisma
QuestionDrizzle ORMPrisma ORM
Where are the tables described?In TypeScript: db/schema.ts.In schema.prisma, a file in Prisma's own language (Prisma ORM 7).
What does a query look like?Like SQL: select, from, where, orderBy.An object of options: findMany({ where, orderBy }).
Is there a step after you change the tables?No: the types come from the schema file itself.Yes: prisma generate writes the client into your project.
How are tables made and changed?drizzle-kit push, or drizzle-kit generate then drizzle-kit migrate.prisma migrate dev while you work, prisma migrate deploy for production.
Latest on npm, October 30, 2026drizzle-orm 0.45.4, with a 1.0 release candidate on its rc tag.@prisma/client 7.10.1. The prisma command-line tool's latest is 8.0.0-rc.28, a Prisma ORM 8 release candidate.

Both work the same way with Server Components: a client in a server-only module, queries in your data functions. Drizzle stays close to SQL and needs no generate step. Prisma hides the SQL behind its model API and comes with its own tools, like Prisma Studio for browsing the data. Choose the one whose queries your team reads more easily.

Connection pooling

A connection is an open line between your server and the database. Opening one takes time before the first query can go: the network handshake, often encryption, and signing in. So a server keeps a few connections open and lends one to each query: a connection pool. With a Postgres server, the Pool from pg, a widely used Postgres driver for Node.js (8.23.1), does this, and Drizzle uses it. This setup isn't recorded, because it needs a database server:

db/index.ts with a Postgres server: a pool behind Drizzle (not recorded)TypeScript
import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";

// Up to 10 open connections, shared by every query this server instance sends.
const pool = new Pool({ connectionString: process.env.DATABASE_URL, max: 10 });

export const db = drizzle({ client: pool });

Line 2 imports Pool. Line 5 makes one, with the address from DATABASE_URL and at most 10 connections, which is also pg's default. Line 7 gives it to Drizzle, from drizzle-orm/node-postgres. A query borrows a connection, sends its SQL, and gives the connection back; if all 10 are busy, it waits for one. (PGlite, in the recordings, runs inside the app and needs no connection at all.)

The pool lives inside one server process. Run more processes, and each has its own:

Three windows, server 1, server 2 and server 3, each with a pool of at most 10 connections. An arrow labeled 10 goes from each to a Postgres window, which shows 30 connections and a limit of 100.server 1pool: max 10server 2pool: max 10server 3pool: max 10101010Postgres30 connectionslimit: 1003 instances × max 10 = up to 30 connections
Every server instance opens its own pool. Three instances with max: 10 can hold 30 connections to the database at once, and each new instance adds 10 more, until the database's limit stops them.

Each row is one way the app might run: the pool size, and the connections the database sees.

Pools add up
Where the app runsPool size (max)Connections the database sees
One server running next start10Up to 10.
3 serverless instances10 eachUp to 30.
50 instances on a busy day10 eachUp to 500: five times Postgres's usual default limit of 100.
50 instances, through a pooler10 each, to the poolerOnly the pooler's own few connections.

A serverless host starts more instances when traffic grows, so the count isn't up to you. Two things keep the database safe: a small max for each instance, and a pooler, a server between your app and the database that takes many connections from your instances and passes their queries over a few of its own. PgBouncer is a well-known one, and many hosts give you a pooled connection string beside the direct one. Use the pooled one in the app, and the direct one for migrations.

One client during development

next start loads db/index.ts once in each server process, and every request shares what it made. While you work, Prisma's docs warn, it's different: after you save a change, next dev reloads server code so the page shows your change without a restart, and if db/index.ts runs again that way, it opens another client, with another pool, while the old one stays open. The recordings run next build and next start, so they can't show next dev; this program stands in for it: each call to runDbFile is one run of the file. Press Edit & run.

Three runs of db/index.ts, each opening a clientPop out in the code editor (opens in a new tab)JavaScript
// Each call stands in for next dev running db/index.ts again.
let opened = 0;

function runDbFile() {
  const client = { number: ++opened };
  return client;
}

runDbFile();
runDbFile();
runDbFile();
console.log(`Clients opened: ${opened}`);

Line 5 makes a new client on every run, and line 12 prints Clients opened: 3. The fix keeps the client on globalThis, JavaScript's global object, which outlives a module that runs again:

The same runs, with the client kept on globalThisPop out in the code editor (opens in a new tab)JavaScript
// Each call stands in for next dev running db/index.ts again.
let opened = 0;

function runDbFile() {
  const client = globalThis.teaClient ?? { number: ++opened };
  globalThis.teaClient = client;
  return client;
}

runDbFile();
runDbFile();
runDbFile();
console.log(`Clients opened: ${opened}`);

Line 5 takes globalThis.teaClient when it's there, and makes a client only when it isn't. Line 6 stores it. Line 13 prints Clients opened: 1. Change line 6 into a comment and run it again: the count goes back to 3.

Two halves. Left, without globalThis: save 1, save 2 and save 3 each open a client, client 1, client 2 and client 3, so three clients are open. Right, with globalThis: all three saves point to the same client 1, so one client is open.without globalThissave 1client 1save 2client 2save 3client 33 clients openwith globalThissave 1save 2save 3client 11 client open
Three saves while next dev runs. Without globalThis, each run of db/index.ts opens a new client; with it, every run finds the first one. The two programs print the same counts: 3 and 1.

Here is the tea shop's db/index.ts with the same fix, built and visited three times. A production build can only show the production side: one client for the whole server process.

One client, kept on globalThis outside productionNext.js
app/db/lib/
db/index.ts
import { PGlite } from "@electric-sql/pglite";import { drizzle } from "drizzle-orm/pglite";import { seed } from "./seed"; // next dev runs this file again after you save a change. A value on// globalThis survives that, so the app keeps one client.const globalForDb = globalThis as unknown as { teaClient?: PGlite }; function openDatabase() {  console.log("Opening the database");  return new PGlite();} const client = globalForDb.teaClient ?? openDatabase();if (process.env.NODE_ENV !== "production") globalForDb.teaClient = client; export const db = drizzle({ client, logger: true }); await client.exec(  "CREATE TABLE IF NOT EXISTS teas (id text PRIMARY KEY, name text NOT NULL, price integer NOT NULL, stock integer DEFAULT 0 NOT NULL)",);await seed(db);
Recorded from Next.js 16.4.3
$ next build▲ Next.js 16.4.3 (Turbopack)- Cache Components enabled- Partial Prefetching enabled   Creating an optimized production build ...✓ Compiled successfully  Running TypeScript ...  Finished TypeScript ...  Collecting page data ...Opening the databaseQuery: insert into "teas" ("id", "name", "price", "stock") values ($1, $2, $3, $4), ($5, $6, $7, $8), ($9, $10, $11, $12) on conflict do nothing -- params: ["green", "Green tea", 4, 12, "mint", "Mint tea", 3, 0, "lemon", "Lemon tea", 3, 5]Opening the databaseQuery: insert into "teas" ("id", "name", "price", "stock") values ($1, $2, $3, $4), ($5, $6, $7, $8), ($9, $10, $11, $12) on conflict do nothing -- params: ["green", "Green tea", 4, 12, "mint", "Mint tea", 3, 0, "lemon", "Lemon tea", 3, 5]✓ Generating static pages (3/3)  Finalizing page optimization ... Route (app)┌ ◐ /└ ○ /_not-found ○  (Static)             prerendered as static content◐  (Partial Prerender)  prerendered as static HTML with dynamic server-streamed content $ next start▲ Next.js 16.4.3- Local:         http://127.0.0.1:3000- Network:       http://127.0.0.1:3000✓ ReadyOpening the databaseQuery: insert into "teas" ("id", "name", "price", "stock") values ($1, $2, $3, $4), ($5, $6, $7, $8), ($9, $10, $11, $12) on conflict do nothing -- params: ["green", "Green tea", 4, 12, "mint", "Mint tea", 3, 0, "lemon", "Lemon tea", 3, 5]Query: select "id", "name", "price" from "teas" order by "teas"."name" ascQuery: select "id", "name", "price" from "teas" order by "teas"."name" ascQuery: select "id", "name", "price" from "teas" order by "teas"."name" asc

Line 7 gives globalThis a type with an optional teaClient. Line 14 uses the client already there, or calls openDatabase on lines 9 to 12. Line 15 stores it, but only outside production: next start sets NODE_ENV to production and runs the file once per process anyway. The Terminal shows that production reuse: one Opening the database after Ready, and one select for each of the three visits. Prisma's docs write the same pattern for its client:

lib/prisma.ts (Prisma ORM 7, not recorded)TypeScript
import { PrismaClient } from "../app/generated/prisma/client";
import { PrismaPg } from "@prisma/adapter-pg";

const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };

const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
const prisma = globalForPrisma.prisma ?? new PrismaClient({ adapter });

if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;

export default prisma;

Line 4 types globalThis, line 7 reuses globalForPrisma.prisma or makes a client with the pg adapter from line 6, and line 9 stores it outside production.

Seeding data

Seeding puts the first rows into a new database: the teas the shop opens with, or made-up data to work with. Here is the shop's seed, from the recorded project:

db/seed.ts: the first three teasTypeScript
import type { db } from "./index";
import { teas } from "./schema";

export async function seed(database: typeof db) {
  await database
    .insert(teas)
    .values([
      { id: "green", name: "Green tea", price: 4, stock: 12 },
      { id: "mint", name: "Mint tea", price: 3, stock: 0 },
      { id: "lemon", name: "Lemon tea", price: 3, stock: 5 },
    ])
    .onConflictDoNothing();
}

Line 1 imports only the type of db, so seed takes whatever client db/index.ts makes. Lines 5 to 11 insert three teas in one statement, and line 12, onConflictDoNothing(), skips any tea whose id is already in the table. So the seed is safe to run again: the second time, it changes nothing. The overview's Terminal shows the SQL: insert into "teas" … on conflict do nothing, with the twelve values as parameters.

A database server keeps its rows between runs, so you seed each new or reset database, from your terminal, after its tables exist, and run the seed again when the seed data changes. With Drizzle, npx drizzle-kit push makes the tables from the schema (a drizzle.config.ts file tells it where the schema and the database are), and a small script calls the seed:

scripts/seed.ts: seeding a database server (not recorded)TypeScript
// scripts/seed.ts: run it once, with npx tsx scripts/seed.ts
import { db } from "../db";
import { seed } from "../db/seed";

await seed(db);
console.log("Seeded the teas");

npx tsx runs a TypeScript file with Node.js. Prisma ORM 7 runs its seed with npx prisma db seed, which runs the command set in prisma.config.ts. For many rows of made-up data, Drizzle has a separate package, drizzle-seed. In the recordings, the database lives in memory and starts empty, so db/index.ts makes the table and seeds it each time a server process starts, on lines 11 to 14:

db/index.ts, from the recorded projectTypeScript
import { PGlite } from "@electric-sql/pglite";
import { drizzle } from "drizzle-orm/pglite";
import { seed } from "./seed";

console.log("Opening the database");
const client = new PGlite();
export const db = drizzle({ client, logger: true });

// In memory, the database starts empty each time the app starts. A database
// server keeps its tables: there, drizzle-kit makes them, and you seed once.
await client.exec(
  "CREATE TABLE IF NOT EXISTS teas (id text PRIMARY KEY, name text NOT NULL, price integer NOT NULL, stock integer DEFAULT 0 NOT NULL)",
);
await seed(db);

Setting up the tea shop's database

Every part of the setup runs at its own time: from your terminal or a deploy step, once in each server process, or for every request. Sort them:

When does this run?
  • npx drizzle-kit migrate
  • npx tsx scripts/seed.ts
  • npx prisma generate (Prisma ORM 7)
  • new Pool({ connectionString, max: 10 })
  • drizzle({ client: pool })
  • db.select().from(teas) in getTeas()
  • await connection()
Try it yourself
0 of 7 correct

Sort each piece: does it run from a terminal or a deploy step, once for each server process, or for each request?

Choose a category for every card. You can change an answer at any time; Reset clears them all.

The pattern behind the sorter: tables and seeds are set up before the app serves anyone, the client opens when a server process loads db/index.ts, and only the queries run for each request.

Common mix-ups

  • "You need an ORM to use a database in Next.js." No. The last lesson's page sent plain SQL. An ORM writes the SQL and types the rows for you; the database doesn't care which you use.
  • "Each request should open a connection and close it after." No. Opening one takes time, so a pool keeps a few open for every request to share. The recordings opened the database once for all their visits.
  • "The globalThis line is for production." No. It stores the client only outside production, for next dev, which can run the file again after each save.
  • "Pooling only matters for big apps." No. On a serverless host, every instance has its own pool, so even a small app on a busy day can run out of connections.
  • "The seed runs every time the app starts." Only in these recordings, whose database lives in memory. A database server keeps its rows: you seed each new or reset database.
  • "npm install prisma gives me Prisma ORM 7." Not on October 30, 2026: its latest is a Prisma ORM 8 release candidate. Ask for prisma@7 if you follow the Prisma 7 setup.

Practice exercises

Most of these exercises are about Next.js projects, which can't run inside this page; the last one runs in the page. Answer first, then use the hints.

Exercise 1 · Warm-upRead a where

This is lib/teas.ts with a where on line 9, and the seed from db/seed.ts. Which teas does the page list?

Starter codeTSX
import "server-only";
import { connection } from "next/server";
import { asc, gt } from "drizzle-orm";
import { db } from "@/db";
import { teas } from "@/db/schema";

export async function getTeas() {
  await connection();
  return db.select({ id: teas.id, name: teas.name, price: teas.price }).from(teas).where(gt(teas.stock, 0)).orderBy(asc(teas.name));
}

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upCount the clients

    next start served two visits to /. How many Opening the database lines did it print?

    Starter codeTSX
    import { PGlite } from "@electric-sql/pglite";
    import { drizzle } from "drizzle-orm/pglite";
    import { seed } from "./seed";
    
    console.log("Opening the database");
    const client = new PGlite();
    export const db = drizzle({ client, logger: true });
    
    // In memory, the database starts empty each time the app starts. A database
    // server keeps its tables: there, drizzle-kit makes them, and you seed once.
    await client.exec(
      "CREATE TABLE IF NOT EXISTS teas (id text PRIMARY KEY, name text NOT NULL, price integer NOT NULL, stock integer DEFAULT 0 NOT NULL)",
    );
    await seed(db);
    

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeList the sold-out teas

      The shop wants a page of teas to reorder. Change getTeas so it returns only the teas with no stock left. Which tea does the page list?

      Starter codeTSX
      import "server-only";
      import { connection } from "next/server";
      import { asc } from "drizzle-orm";
      import { db } from "@/db";
      import { teas } from "@/db/schema";
      
      export async function getTeas() {
        await connection();
        return db.select({ id: teas.id, name: teas.name, price: teas.price }).from(teas).orderBy(asc(teas.name));
      }
      

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeAdd up the connections

        The shop runs on a serverless host. On a busy morning, it runs four instances, each with new Pool({ max: 10 }). How many connections can the database see at most?

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeThe seed that runs twice

          This db/index.ts runs the seed twice, as a file that runs again with the same database would, and db/seed.ts has lost its .onConflictDoNothing(). The build stops. Why?

          Starter codeTSX
          import { PGlite } from "@electric-sql/pglite";
          import { drizzle } from "drizzle-orm/pglite";
          import { seed } from "./seed";
          
          console.log("Opening the database");
          const client = new PGlite();
          export const db = drizzle({ client, logger: true });
          
          // In memory, the database starts empty each time the app starts. A database
          // server keeps its tables: there, drizzle-kit makes them, and you seed once.
          await client.exec(
            "CREATE TABLE IF NOT EXISTS teas (id text PRIMARY KEY, name text NOT NULL, price integer NOT NULL, stock integer DEFAULT 0 NOT NULL)",
          );
          await seed(db);
          await seed(db);
          

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeKeep one client

            Change runDbFile so the three runs share one client, the way db/index.ts does with globalThis. What does the program print then?

            Starter codePop out in the code editor (opens in a new tab)JavaScript
            // Each call stands in for next dev running db/index.ts again.
            let opened = 0;
            
            function runDbFile() {
              const client = { number: ++opened };
              return client;
            }
            
            runDbFile();
            runDbFile();
            runDbFile();
            console.log(`Clients opened: ${opened}`);

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              For each question, ask when the code runs: once, once per server process, or for each request.

              The Connecting a database quiz · 7 questionsScore: first tries count
              1. Question 1 of 7What does an ORM like Drizzle do between your code and the database?

                Choose an answer to see the explanation.

              2. Question 2 of 7Which SQL does this version of lib/teas.ts send?

                Read the code, then predictTSX
                import "server-only";
                import { connection } from "next/server";
                import { asc, gt } from "drizzle-orm";
                import { db } from "@/db";
                import { teas } from "@/db/schema";
                
                export async function getTeas() {
                  await connection();
                  return db.select({ id: teas.id, name: teas.name, price: teas.price }).from(teas).where(gt(teas.stock, 0)).orderBy(asc(teas.name));
                }
                

                Choose an answer to see the explanation.

              3. Question 3 of 7Why does a server keep a pool of open connections instead of opening one for each query?

                Choose an answer to see the explanation.

              4. Question 4 of 750 serverless instances each open a pool with max: 10. What helps the database most?

                Choose an answer to see the explanation.

              5. Question 5 of 7Why does db/index.ts keep its client on globalThis when NODE_ENV isn't "production"?

                Choose an answer to see the explanation.

              6. Question 6 of 7What does .onConflictDoNothing() on line 12 do when the seed runs a second time?

                Read the code, then predictTSX
                import type { db } from "./index";
                import { teas } from "./schema";
                
                export async function seed(database: typeof db) {
                  await database
                    .insert(teas)
                    .values([
                      { id: "green", name: "Green tea", price: 4, stock: 12 },
                      { id: "mint", name: "Mint tea", price: 3, stock: 0 },
                      { id: "lemon", name: "Lemon tea", price: 3, stock: 5 },
                    ])
                    .onConflictDoNothing();
                }
                

                Choose an answer to see the explanation.

              7. Question 7 of 7On October 30, 2026, what does npm install prisma install?

                Choose an answer to see the explanation.

              Key takeaways

              • Postgres is a common database for a Next.js app. Keep its connection string in a server environment variable, like DATABASE_URL.
              • An ORM describes your tables in code and writes the SQL, with values as parameters. Drizzle reads like SQL; Prisma uses models and a generated client.
              • Each server process opens one client, with a pool of a few connections. On serverless hosts, pools add up: keep them small and use a pooler.
              • While you work, next dev can run db/index.ts again. Keep the client on globalThis outside production.
              • Make tables with the ORM's migration tool, and seed each new or reset database with a seed that's safe to run twice: onConflictDoNothing().
              • Versions move fast: on October 30, 2026, Drizzle's docs install a 1.0 release candidate and npm install prisma gives a Prisma ORM 8 release candidate. Pin what you follow.

              Remember the one-liner.
              Connect once per process, pool a few connections, let the ORM write the SQL, and seed with a script you can run twice.

              Coming next: Parallel and sequential fetching. It finds request waterfalls between components and removes them, with Promise.all and React's cache().

              CompleteFrontend Clear concepts. Working examples.