React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

prismapostgresqldatabaseormserver-actionsfullstack

Next.js + Prisma + PostgreSQL

A full-stack Next.js 15 setup with Prisma ORM, PostgreSQL database, and Server Actions for mutations.

Recipe

  1. Scaffold a Next.js 15 app with Tailwind and TypeScript:
    npx create-next-app@latest my-app --typescript --tailwind --app
    cd my-app
  2. Install Prisma and the client:
    npm install prisma @prisma/client
    npm install -D prisma
  3. Initialize Prisma with PostgreSQL:
    npx prisma init --datasource-provider postgresql
  4. Define your schema in prisma/schema.prisma.
  5. Set DATABASE_URL in .env.
  6. Run your first migration:
    npx prisma migrate dev --name init
  7. Create a Prisma client singleton at lib/prisma.ts to avoid exhausting connections during hot-reload.

Working Example

A complete Todo app with Prisma + PostgreSQL + Server Actions.

prisma/schema.prisma

generator 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())
  updatedAt DateTime @updatedAt
\}

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(\{
    log: process.env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"],
  \});
 
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;

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 (
    <main className="mx-auto max-w-xl p-8">
      <h1 className="text-2xl font-bold">Todos</h1>
 
      <form action=\{addTodo\} className="mt-4 flex gap-2">
        <input
          name="title"
          required
          className="flex-1 rounded border px-3 py-2"
          placeholder="New todo..."
        />
        <button type="submit" className="rounded bg-black px-4 py-2 text-white">
          Add
        </button>
      </form>
 
      <ul className="mt-6 space-y-2">
        \{todos.map((todo) => (
          <li key=\{todo.id\} className="rounded border p-3">
            \{todo.title\}
          </li>
        ))\}
      </ul>
    </main>
  );
\}

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");
  if (typeof title !== "string" || title.trim() === "") return;
 
  await prisma.todo.create(\{
    data: \{ title: title.trim() \},
  \});
 
  revalidatePath("/");
\}

Deep Dive

How It Works

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.

Variations

  • Hosted PostgreSQL: Supabase, Neon, Railway, or Vercel Postgres all work — just paste their connection string into DATABASE_URL.
  • Local development with SQLite: change provider = "sqlite" and use url = "file:./dev.db" for a zero-config local DB.
  • Seeding: add a prisma/seed.ts script and register it under "prisma": \{ "seed": "tsx prisma/seed.ts" \} in package.json, then run npx prisma db seed.
  • Prisma Studio: run npx prisma studio for a browser-based data browser and editor.
  • Prisma Accelerate: connection pooling + global caching through a single prisma:// URL, ideal for serverless deployments.
  • Alternative ORM: Drizzle ORM offers typed SQL and a lighter runtime.

TypeScript Notes

  • Prisma auto-generates types for every model. Import with import \{ Todo \} from "@prisma/client".
  • For query results with relations, use Prisma.TodoGetPayload<\{ include: \{ author: true \} \}> to derive an exact type of the returned shape.
  • Server Actions can be typed as (formData: FormData) => Promise<void> or use the newer useActionState pattern with a prevState argument.
  • Enable strict mode in tsconfig.json to catch nullable fields — Prisma honors ? in schema as T | null in TS.

Gotchas

  1. Singleton or die. Without the globalThis singleton in lib/prisma.ts, Next.js dev mode will leak connections until Postgres refuses new ones.
  2. Missing DATABASE_URL. Prisma won't even generate without it. Put it in .env (not just .env.local) because the Prisma CLI reads .env.
  3. Forgetting 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.
  4. Production migrations. Use prisma migrate deploy in CI/CD, never prisma migrate dev — the latter can prompt interactively and drop data.
  5. Binary targets for deployment. On platforms like Vercel or AWS Lambda you may need to add binaryTargets = ["native", "rhel-openssl-3.0.x"] to the generator block, or your deployment will fail with "Query engine binary not found."
  6. Connection limits in serverless. Each Lambda/Edge invocation can open a fresh connection. Use PgBouncer, Prisma Accelerate, or a pooled connection string.
  7. Edge runtime incompatibility. The standard Prisma client does not run on Edge. Use Prisma Accelerate or the Driver Adapters preview for edge support.

Alternatives

ToolStyleBest For
PrismaORM with schema DSLFull-featured typed ORM, migrations, Studio
Drizzle ORMTyped SQL builderLightweight, edge-friendly, closer to SQL
KyselyTyped query builderTeams that want SQL-first with TS inference
postgres.jsRaw SQL driverMaximum control, no abstraction overhead
Supabase clientREST/Realtime SDKAuth + DB + realtime in one package

FAQs

Why do I need a Prisma singleton in Next.js?

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.

What's the difference between 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.

Can I use Prisma with Edge runtime?

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.

How do I run migrations in production?

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.

How do I seed the database?

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.

Gotcha: my Vercel deployment fails with "Query engine binary not found"

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).

Gotcha: my types aren't updating after I changed the schema

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.

TypeScript: how do I type a query result that includes relations?

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.

TypeScript: how do I type a Server Action that takes a form?

A plain action takes FormData and returns Promise<void>:

export async function addTodo(formData: FormData): Promise<void> \{ /* ... */ \}

For useActionState you add a prevState parameter:

export async function addTodo(prevState: State, formData: FormData): Promise<State> \{ /* ... */ \}
Should I call Prisma from Client Components?

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.

How does 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.

Should I use Prisma or Drizzle?

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.