Search across all documentation pages
A full-stack Next.js 15 setup with Prisma ORM, PostgreSQL database, and Server Actions for mutations.
npx create-next-app@latest my-app --typescript --tailwind --app
cd my-appnpm install prisma @prisma/client
npm install -D prismanpx prisma init --datasource-provider postgresqlprisma/schema.prisma.DATABASE_URL in .env.npx prisma migrate dev --name initlib/prisma.ts to avoid exhausting connections during hot-reload.A complete Todo app with Prisma + PostgreSQL + Server Actions.
prisma/schema.prismagenerator client \{
provider = "prisma-client-js"
\}
datasource db \{
provider = "postgresql"
url = env("DATABASE_URL")
\}
model Todo \{
id String @id @default(cuid())
title String
completed Boolean @default(false)
createdAt DateTime @default(now())
lib/prisma.ts (singleton)import \{ PrismaClient \} from "@prisma/client";
const globalForPrisma = globalThis as unknown as \{
prisma: PrismaClient | undefined;
\};
export const prisma =
globalForPrisma.prisma ??
new PrismaClient
app/page.tsx (Server Component)import \{ prisma \} from "@/lib/prisma";
import \{ addTodo \} from "./actions";
export default async function Home() \{
const todos = await prisma.todo.findMany(\{
orderBy: \{ createdAt: "desc" \},
\});
return
app/actions.ts (Server Action)"use server";
import \{ prisma \} from "@/lib/prisma";
import \{ revalidatePath \} from "next/cache";
export async function addTodo(formData: FormData) \{
const title = formData.get("title");
Prisma generates a fully typed client based on your schema. The Next.js Server Component runs on the server and queries Postgres directly through the Prisma client. Server Actions are POST endpoints created automatically by Next.js — form submissions invoke the action, which mutates the database and calls revalidatePath to purge the cached render so the new data appears on the next request.
The singleton pattern is critical in development. Next.js hot reload re-evaluates modules on every change, and without a singleton you would create a fresh PrismaClient per reload, quickly exhausting the database connection pool.
DATABASE_URL.provider = "sqlite" and use url = "file:./dev.db" for a zero-config local DB.prisma/seed.ts script and register it under "prisma": \{ "seed": "tsx prisma/seed.ts" \} in package.json, then run npx prisma db seed.npx prisma studio for a browser-based data browser and editor.prisma:// URL, ideal for serverless deployments.import \{ Todo \} from "@prisma/client".Prisma.TodoGetPayload<\{ include: \{ author: true \} \}> to derive an exact type of the returned shape.(formData: FormData) => Promise<void> or use the newer useActionState pattern with a prevState argument.tsconfig.json to catch nullable fields — Prisma honors ? in schema as T | null in TS.globalThis singleton in lib/prisma.ts, Next.js dev mode will leak connections until Postgres refuses new ones.DATABASE_URL. Prisma won't even generate without it. Put it in .env (not just .env.local) because the Prisma CLI reads .env.prisma generate. After changing schema.prisma, regenerate the client or your types drift. prisma migrate dev does this automatically, but prisma db push and manual schema edits may not.prisma migrate deploy in CI/CD, never prisma migrate dev — the latter can prompt interactively and drop data.binaryTargets = ["native", "rhel-openssl-3.0.x"] to the generator block, or your deployment will fail with "Query engine binary not found."| Tool | Style | Best For |
|---|---|---|
| Prisma | ORM with schema DSL | Full-featured typed ORM, migrations, Studio |
| Drizzle ORM | Typed SQL builder | Lightweight, edge-friendly, closer to SQL |
| Kysely | Typed query builder | Teams that want SQL-first with TS inference |
| postgres.js | Raw SQL driver | Maximum control, no abstraction overhead |
| Supabase client | REST/Realtime SDK | Auth + DB + realtime in one package |
Next.js dev mode reloads modules on file changes. Each reload creates a new PrismaClient instance if you naively new PrismaClient() at module scope, and each instance opens its own connection pool. Within minutes you hit your database's connection limit. The globalThis singleton survives hot-reload because it lives on the global object, not the module.
prisma migrate dev and prisma db push?migrate dev generates SQL migration files under prisma/migrations/ that you commit to version control — it's the production-safe workflow. db push syncs the schema directly without creating migration files — use it for rapid prototyping only.
Not with the standard client. You need Prisma Accelerate (@prisma/extension-accelerate) or the Driver Adapters (currently preview) which route queries through a fetch-based proxy instead of Node TCP.
Run npx prisma migrate deploy as part of your build or release step. This applies any pending migrations from prisma/migrations/ without prompting and without generating new ones.
Create prisma/seed.ts, add "prisma": \{ "seed": "tsx prisma/seed.ts" \} to package.json, then run npx prisma db seed. It also runs automatically after prisma migrate reset.
Add the correct binaryTargets to schema.prisma:
generator client \{
provider = "prisma-client-js"
binaryTargets = ["native", "rhel-openssl-3.0.x"]
\}Then commit, redeploy, and ensure prisma generate runs in your build ("postinstall": "prisma generate" in package.json is the standard fix).
Run npx prisma generate manually. Your editor's TS server may also need a restart. prisma migrate dev normally runs generate for you, but db push and direct schema edits without migration do not.
Use Prisma.<Model>GetPayload:
import \{ Prisma \} from "@prisma/client";
type TodoWithAuthor = Prisma.TodoGetPayload<\{
include: \{ author: true \};
\}>;This gives you a precise type matching the exact shape findMany(\{ include: \{ author: true \} \}) returns.
A plain action takes FormData and returns Promise<void>:
export async function addTodo(formData: FormData): Promise<void> \{ /* ... */ \}For useActionState you add a prevState parameter:
No. Prisma only runs server-side. Call it from Server Components, Server Actions, Route Handlers, or generateStaticParams. If a Client Component needs data, pass it down as props or fetch through a Server Action/Route Handler.
revalidatePath interact with Prisma mutations?revalidatePath("/") tells Next.js to discard the cached render for that path. On the next request, the Server Component re-runs, re-queries Prisma, and gets the updated data. Without it, users see stale data until the cache expires.
Prisma has better DX, Studio, and migrations. Drizzle is lighter, edge-native, and gives you SQL-like syntax with strong types. Pick Prisma for most full-stack apps; pick Drizzle if you care about bundle size, edge compatibility, or want to stay close to SQL.
export async function addTodo(prevState: State, formData: FormData): Promise<State> \{ /* ... */ \}