Shipping a Next.js App Router app that uses API Routes and Prisma is straightforward once you lock down a few production‑critical details: environment variables, database provisioning, connection pooling, migrations, and runtime configuration. This guide is a step‑by‑step, copy‑paste playbook from local to production.
TL;DR
- Stack: Next.js (App Router) + API Routes + Prisma ORM.
- Database: Postgres (Neon, Supabase, Railway, PlanetScale for MySQL variant).
- Deploy: Vercel one‑click or Docker + any container host.
- Must‑haves: env vars, Prisma migrations in CI/CD, connection pooling, health checks, logging.
- Security: strict env handling, schema validation, avoid long‑lived DB connections, rate limiting.
1) Create the Project (or adapt your existing app)
# App Router template
pnpm dlx create-next-app@latest my-app --ts --app --eslint --src-dir=false --import-alias "@/*"
cd my-app
pnpm add prisma @prisma/client zod
pnpm add -D tsx prisma-docs-generatorInitialize Prisma:
pnpm dlx prisma init --datasource-provider postgresqlThis creates prisma/schema.prisma and .env.
Minimal Prisma schema (users + posts)
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(cuid())
email String @unique
name String?
createdAt DateTime @default(now())
posts Post[]
}
model Post {
id String @id @default(cuid())
title String
content String?
published Boolean @default(false)
createdAt DateTime @default(now())
authorId String
author User @relation(fields: [authorId], references: [id])
}Apply initial migration locally:
pnpm prisma migrate dev --name initGenerate the client:
pnpm prisma generate2) Add a Prisma Client helper with safe re‑use (hot‑reload friendly)
// lib/prisma.ts
import { PrismaClient } from "@prisma/client";
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined };
export const prisma =
globalForPrisma.prisma ??
new PrismaClient({
log: ["error", "warn"],
});
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;- Prevents multiple clients during dev hot‑reloads.
- Restrict logs in production; consider
querylogs only when debugging.
3) Example API Route using Prisma
// app/api/posts/route.ts
import { NextResponse } from "next/server";
import { z } from "zod";
import { prisma } from "@/lib/prisma";
const CreatePostBody = z.object({
title: z.string().min(1),
content: z.string().optional(),
});
export const runtime = "nodejs"; // uses Node runtime (good for Prisma drivers)
export const GET = async () => {
try {
const posts = await prisma.post.findMany({ orderBy: { createdAt: "desc" } });
return NextResponse.json({ success: true, data: posts }, { status: 200 });
} catch (error) {
return NextResponse.json({ success: false, message: "Failed to fetch posts" }, { status: 500 });
}
};
export const POST = async (request: Request) => {
try {
const json = await request.json();
const parsed = CreatePostBody.safeParse(json);
if (!parsed.success) {
return NextResponse.json({ success: false, message: "Invalid body" }, { status: 400 });
}
const post = await prisma.post.create({ data: parsed.data });
return NextResponse.json({ success: true, data: post }, { status: 201 });
} catch (error) {
return NextResponse.json({ success: false, message: "Unexpected server error" }, { status: 500 });
}
};Notes:
- Prefer
runtime = "nodejs"for Prisma with Data Proxy disabled. - For Edge runtime, use Prisma Data Proxy or HTTP connector.
4) Environment variables
Create .env locally and configure secrets in your host (Vercel, Render, Fly, etc.).
# .env
DATABASE_URL="postgresql://USER:PASSWORD@HOST:PORT/DB?schema=public"
NEXT_PUBLIC_APP_URL="http://localhost:3000"- Keep
.envout of VCS; use.env.exampleto document keys. - In Vercel, add them in Project Settings → Environment Variables.
Create an example file:
printf "DATABASE_URL=\nNEXT_PUBLIC_APP_URL=\n" > .env.example5) Local run
pnpm dev
# visit http://localhost:3000Validate your API routes:
curl -s http://localhost:3000/api/posts | jqCreate a post:
curl -s -X POST http://localhost:3000/api/posts \
-H 'Content-Type: application/json' \
-d '{"title":"Hello","content":"First post"}' | jq6) Provision a production Postgres
Pick one:
- Neon: generous free tier, serverless, connection pooling.
- Supabase: Postgres + auth + storage.
- Railway/Render/Fly/Crunchy: managed Postgres.
Grab the connection string and set DATABASE_URL in your host.
Pooling tip: If your platform has connection limits, enable PgBouncer / pooled connection string.
7) Deployment Path A - Vercel (recommended)
- Push to GitHub/GitLab.
- Import repo in Vercel → Select framework: Next.js.
- Set env vars:
DATABASE_URL,NEXT_PUBLIC_APP_URL. - Build command: default (
next build).
Run migrations on deploy:
# Add a postbuild script to run Prisma migrations on Vercel
pnpm pkg set scripts.postbuild="prisma migrate deploy"Or explicitly in Vercel Project → Build & Development Settings → Post‑install Command: pnpm prisma migrate deploy.
If using Prisma Data Proxy (optional for Edge runtime):
pnpm prisma generate --data-proxySet export const runtime = "edge" in specific routes only when using Data Proxy/HTTP connector.
8) Deployment Path B - Docker + any host
Add a Dockerfile:
# Dockerfile
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm fetch
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules /app/node_modules
COPY . .
RUN corepack enable && pnpm install --frozen-lockfile
RUN pnpm prisma generate
RUN pnpm build
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
# Set to node runtime for Prisma
ENV NEXT_RUNTIME=nodejs
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
# copy prisma folder for migrations at runtime if needed
COPY --from=builder /app/prisma ./prisma
EXPOSE 3000
CMD ["node", "server.js"]Build and run:
docker build -t my-app:latest .
docker run -p 3000:3000 \
-e DATABASE_URL="postgresql://USER:PASSWORD@HOST:PORT/DB?schema=public" \
-e NEXT_PUBLIC_APP_URL="http://localhost:3000" \
my-app:latestRun migrations in entrypoint or separate job:
docker run --rm -e DATABASE_URL=... my-app:latest node -e "await (await import('prisma')).migrate?.deploy?.()"
# or simply exec inside the container
# docker exec <cid> pnpm prisma migrate deployConsider a minimal docker-compose.yml for local + db:
version: "3.9"
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres
POSTGRES_DB: app
ports:
- "5432:5432"
web:
build: .
environment:
DATABASE_URL: postgresql://postgres:postgres@db:5432/app?schema=public
NEXT_PUBLIC_APP_URL: http://localhost:3000
ports:
- "3000:3000"
depends_on:
- db9) CI/CD: run migrations safely
GitHub Actions example:
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: ["main"]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with: { version: 9 }
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: pnpm install --frozen-lockfile
- run: pnpm prisma generate
- run: pnpm build
- run: pnpm prisma migrate deploy
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}- Use
migrate deployin CI for idempotent production migrations. - Avoid
migrate devin CI; it’s for local development.
10) Health checks, logging, and metrics
- Add a lightweight health route:
// app/api/health/route.ts
import { NextResponse } from "next/server";
export const GET = async () => NextResponse.json({ ok: true, ts: Date.now() });- Centralize error logging; in Vercel, console output appears in Logs. For advanced needs, add Sentry/Logtail and mask PII.
- Track DB metrics via your provider; watch connection count, slow queries, errors.
11) Common production pitfalls
- Missing
DATABASE_URLin production environments. - Exhausting Postgres connections on serverless without pooling.
- Running
migrate devin CI, causing drift. - Using Edge runtime with Prisma without Data Proxy/HTTP connector.
- Forgetting to rerun
prisma generateafter schema changes.
12) Final production checklist
- Env: all required vars set in host;
.env.exampleupdated. - DB: provisioned with pooling;
prisma migrate deploysucceeds. - Runtime: API routes using Node runtime unless Data Proxy is configured.
- Migrations: run in CI/CD or post‑build hook.
- Health:
/api/healthresponds 200. - Observability: logs aggregated; error alerts set.
- Security: dependencies up‑to‑date; no leaked secrets; rate‑limit sensitive routes.
- Backups: automated DB backups enabled and tested.
With these pieces in place, you can confidently ship a Next.js App Router app with API Routes and Prisma to production on Vercel or any container platform. Copy the snippets, wire up your envs, and deploy. Good luck!
