Drizzle ORM hat sich 2026 als die bevorzugte Datenbanklösung für Next.js-Projekte durchgesetzt – und ehrlich gesagt zu Recht. Im Vergleich zu Prisma bietet Drizzle ein erheblich kleineres Bundle, eine serverless-kompatible Architektur und eine vollständig typsichere Query-API, alles ohne Laufzeit-Codegenerierung. Dieser Leitfaden führt dich Schritt für Schritt durch die Integration: von der Installation über Schema-Definition und Migrationen bis hin zu typsicheren CRUD-Operationen mit React Server Components und Server Actions.
Next.js mit Drizzle ORM und PostgreSQL: Datenbankintegration im App Router (2026)
Schritt-für-Schritt-Integration von Drizzle ORM mit Next.js App Router und PostgreSQL: von Installation und Schema-Definition über Migrationen bis zu typsicheren CRUD-Operationen via Server Actions und Neon Serverless Pooling – vollständig für 2026.

Drizzle ORM unterscheidet sich grundlegend von klassischen ORMs wie Prisma oder TypeORM. Statt zur Laufzeit einen generierten Client zu laden, ist Drizzle ein reines TypeScript-Library, das direkt auf dem Datenbanktreiber sitzt. Das hat drei entscheidende Vorteile für Next.js-Projekte:
- Serverless-First: Drizzle öffnet pro Request eine Verbindung (oder nutzt HTTP wie bei Neon) – kein langlaufender Connection-Pool, der Serverless-Funktionen blockiert oder kalt startet.
- Kein Cold-Start-Overhead: Der Drizzle-Core hat keine Laufzeit-Codegenerierung. Das Bundle bleibt unter 30 kB. Prismas Query Engine benötigt mehrere Megabyte und verlängert den Kaltstart messbar.
- SQL-nah, aber typsicher: Drizzle-Queries lesen sich wie SQL, sind aber vollständig durch TypeScript abgesichert – inklusive automatisch inferierter Rückgabetypen und Compile-Time-Fehler bei falschen Spaltennamen.
Laut dem State of JS 2025-Report hat Drizzle Prisma beim Interesse neuer Projekte überholt, insbesondere bei Teams, die auf Vercel, Railway oder Neon deployen. Das überrascht mich nicht – ich hab' selbst erlebt, wie frustrierend Prismas Cold-Start-Probleme bei Edge-Deployments sein können. Die vollständig serverlose Natur von Drizzle macht es zur idealen Ergänzung für den Next.js App Router.
Voraussetzungen
Kurz bevor wir loslegen: Das brauchst du für diesen Leitfaden.
- Next.js 15 oder 16 mit App Router
- Node.js 18 oder neuer
- Eine PostgreSQL-Datenbank – lokal oder bei Neon, Supabase, Railway oder Fly.io
- TypeScript-Kenntnisse auf mittlerem Niveau
Installation
Je nach Datenbankanbindung gibt es verschiedene Treiberoptionen. Für Vercel-Deployments mit Neon empfiehlt sich der HTTP-Treiber (er ist Edge-kompatibel und braucht keine Node.js-Socket-APIs):
# Mit Neon Serverless (empfohlen für Vercel & Edge)
npm i drizzle-orm @neondatabase/serverless
npm i -D drizzle-kit
# Mit node-postgres (lokale PostgreSQL-Instanz oder Self-Hosted)
npm i drizzle-orm pg
npm i -D drizzle-kit @types/pg
Lege außerdem eine .env-Datei im Projektstamm an:
DATABASE_URL=postgresql://user:password@host:5432/dbname?sslmode=require
Für Neon findest du den Connection String im Dashboard unter Connection Details → Pooled connection. Der gepoolte String leitet über den integrierten PgBouncer – das ist wichtig für Serverless-Umgebungen mit vielen gleichzeitigen Requests.
Projektstruktur
Eine saubere Struktur trennt Datenbanklogik klar vom Rest der Anwendung. So sieht ein bewährtes Layout aus:
project/
├── drizzle/ # Generierte SQL-Migrationen (in Git committen!)
│ ├── 0000_initial.sql
│ └── 0001_add_posts.sql
├── src/
│ ├── app/
│ │ ├── actions/ # Server Actions ('use server')
│ │ └── posts/
│ │ └── page.tsx
│ └── db/
│ ├── index.ts # DB-Client-Singleton + 'server-only'
│ └── schema.ts # pgTable-Definitionen & Relations
├── drizzle.config.ts
└── .env
drizzle.config.ts einrichten
Die Konfigurationsdatei liegt im Projektstamm und steuert drizzle-kit für Migrationen, Studio und Schema-Introspection:
// drizzle.config.ts
import { config } from 'dotenv';
import { defineConfig } from 'drizzle-kit';
config({ path: '.env' });
export default defineConfig({
schema: './src/db/schema.ts',
out: './drizzle', // Ordner für generierte SQL-Migrations-Dateien
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL!,
},
strict: true, // Warnt vor destruktiven Schema-Änderungen
});
Datenbankverbindung: Der Singleton-Pattern
Next.js lädt Module im Development-Modus bei jedem Speichern neu – aber der Node.js-Prozess selbst bleibt am Leben. Ohne Vorkehrungen entstehen bei jedem Hot Reload neue Datenbankverbindungen, die schnell das Connection-Limit von Neon (20 im Free-Tier) erschöpfen. Die Lösung ist ein globalThis-Singleton:
// src/db/index.ts
import 'server-only'; // Verhindert Import in Client Components – Compile-Error statt Runtime-Leak
import { neon } from '@neondatabase/serverless';
import { drizzle } from 'drizzle-orm/neon-http';
import * as schema from './schema';
const globalForDb = globalThis as unknown as {
db: ReturnType<typeof drizzle> | undefined;
};
function createDb() {
const sql = neon(process.env.DATABASE_URL!);
return drizzle({ client: sql, schema });
}
export const db = globalForDb.db ?? createDb();
// In Production ist Caching durch den Module-Cache gewährleistet
if (process.env.NODE_ENV !== 'production') {
globalForDb.db = db;
}
Das import 'server-only' am Anfang der Datei ist entscheidend. Wenn eine Client Component versehentlich db importiert, bricht der Next.js-Build sofort mit einem lesbaren Fehler ab – dein DATABASE_URL gelangt niemals ins Browser-Bundle. Für traditionelle PostgreSQL-Instanzen mit pg verwendest du stattdessen drizzle-orm/node-postgres und einen Pool-Singleton nach demselben Muster.
Schema mit pgTable definieren
Das Schema ist der zentrale Baustein deiner Drizzle-Integration. Alle Tabellen, Indizes und Relationen werden als TypeScript-Code definiert – der Single Source of Truth für Datenbankstruktur und TypeScript-Typen:
// src/db/schema.ts
import {
pgTable, serial, varchar, text,
boolean, integer, timestamp, index, uniqueIndex,
} from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';
export const users = pgTable('users', {
id: serial('id').primaryKey(),
name: varchar('name', { length: 255 }).notNull(),
email: varchar('email', { length: 320 }).unique().notNull(),
image: varchar('image', { length: 500 }),
createdAt: timestamp('created_at').defaultNow().notNull(),
}, (t) => ({
emailIdx: uniqueIndex('users_email_idx').on(t.email),
}));
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
title: varchar('title', { length: 200 }).notNull(),
slug: varchar('slug', { length: 250 }).unique().notNull(),
content: text('content'),
published: boolean('published').default(false).notNull(),
authorId: integer('author_id')
.references(() => users.id, { onDelete: 'cascade' })
.notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
}, (t) => ({
authorIdx: index('posts_author_id_idx').on(t.authorId),
slugIdx: uniqueIndex('posts_slug_idx').on(t.slug),
}));
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
}));
export const postsRelations = relations(posts, ({ one }) => ({
author: one(users, { fields: [posts.authorId], references: [users.id] }),
}));
// Typen direkt aus dem Schema inferieren – kein manuelles Interface nötig
export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;
export type Post = typeof posts.$inferSelect;
export type NewPost = typeof posts.$inferInsert;
Die exportierten Typen ($inferSelect / $inferInsert) sind vollständig von der Datenbankstruktur abgeleitet. Änderst du das Schema – etwa durch Hinzufügen einer Spalte oder Änderung eines Typs – aktualisieren sich alle abhängigen Typen automatisch. Tippfehler in Spaltennamen werden zur Compile-Zeit abgefangen, nicht erst zur Laufzeit. Das klingt selbstverständlich, spart in der Praxis aber enorm viel Debugging-Zeit.
Migrationen mit drizzle-kit
Füge folgende Scripts in deine package.json ein:
{
"scripts": {
"db:push": "drizzle-kit push",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:studio": "drizzle-kit studio"
}
}
Der entscheidende Unterschied zwischen push und generate + migrate:
db:push– Wendet Schema-Änderungen direkt auf die Datenbank an, ohne eine SQL-Datei zu erstellen. Ideal für die lokale Entwicklung bei schnellen Iterationen.db:generate– Erstellt eine neue SQL-Datei imdrizzle/-Ordner (z. B.0001_add_posts.sql), ohne sie anzuwenden. Diese Datei in Git committen und im Team teilen.db:migrate– Spielt alle noch nicht angewendeten SQL-Dateien ein. In CI/CD ausführen – z. B. alspostinstall-Script, damit Vercel jede Migration automatisch vor dem Deploy einspielt.
Migration-Dateien sind ein streng append-only Log – bereits commitete SQL-Dateien niemals nachträglich bearbeiten. Drizzle verfolgt den angewendeten Stand in der Tabelle drizzle.__drizzle_migrations. Für Produktions-Deployments auf Vercel gilt: immer die direkte Connection URL (nicht den Pooler) für Migrationen verwenden, da PgBouncer keine Prepared Statements unterstützt, die drizzle-kit intern nutzt.
Daten lesen in React Server Components
Server Components sind async-Funktionen – Drizzle-Queries können direkt aufgerufen werden. Kein useEffect, kein Loading-Spinner, kein separater API-Endpunkt. So einfach ist das:
// src/app/posts/page.tsx
import { db } from '@/db';
import { posts } from '@/db/schema';
import { eq, desc } from 'drizzle-orm';
export default async function PostsPage() {
// SQL-ähnliche Query-API
const latestPosts = await db
.select()
.from(posts)
.where(eq(posts.published, true))
.orderBy(desc(posts.createdAt))
.limit(20);
// Relational Query mit verschachtelten Daten (Prisma-ähnliche Syntax)
const postsWithAuthors = await db.query.posts.findMany({
where: { published: true },
with: {
author: { columns: { id: true, name: true } },
},
orderBy: { createdAt: 'desc' },
limit: 10,
});
return (
<ul>
{postsWithAuthors.map((post) => (
<li key={post.id}>
<strong>{post.title}</strong> — {post.author.name}
</li>
))}
</ul>
);
}
Da Server Components auf dem Server ausgeführt werden, landet der Datenbankaufruf nie im Client-Bundle. Die zurückgegebenen Daten werden als serialisiertes JSON an den Browser gesendet – kein Sicherheitsrisiko durch offengelegte Queries. Der Drizzle-Query-Builder generiert optimiertes SQL und gibt vollständig getypte Ergebnisse zurück.
Mutationen mit Server Actions
Für Schreiboperationen kombinierst du Drizzle mit Next.js Server Actions im App Router. Diese laufen ausschließlich auf dem Server, können direkt aus Client Components aufgerufen werden und benötigen keinen separaten API-Endpunkt:
// src/app/actions/posts.ts
'use server';
import { db } from '@/db';
import { posts } from '@/db/schema';
import { eq } from 'drizzle-orm';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
import { z } from 'zod';
const createPostSchema = z.object({
title: z.string().min(1).max(200),
content: z.string().optional(),
authorId: z.coerce.number().positive(),
});
// INSERT mit .returning() für die eingefügte Zeile
export async function createPost(formData: FormData) {
const parsed = createPostSchema.safeParse(Object.fromEntries(formData));
if (!parsed.success) return { error: parsed.error.flatten() };
const slug = parsed.data.title
.toLowerCase()
.replace(/\s+/g, '-')
.replace(/[^\w-]+/g, '');
const [newPost] = await db
.insert(posts)
.values({ ...parsed.data, slug })
.returning();
revalidatePath('/posts');
redirect(`/posts/${newPost.slug}`);
}
// UPDATE
export async function togglePublished(id: number, published: boolean) {
await db
.update(posts)
.set({ published, updatedAt: new Date() })
.where(eq(posts.id, id));
revalidatePath('/posts');
}
// DELETE
export async function deletePost(id: number) {
await db.delete(posts).where(eq(posts.id, id));
revalidatePath('/posts');
}
// UPSERT (INSERT oder UPDATE bei Konflikt)
export async function upsertPost(data: typeof posts.$inferInsert) {
await db
.insert(posts)
.values(data)
.onConflictDoUpdate({
target: posts.slug,
set: { title: data.title, content: data.content, updatedAt: new Date() },
});
revalidatePath('/posts');
}
Das zugehörige Client Component mit useActionState:
'use client';
import { createPost } from '@/app/actions/posts';
import { useActionState } from 'react';
export function CreatePostForm({ authorId }: { authorId: number }) {
const [state, action, isPending] = useActionState(createPost, null);
return (
<form action={action}>
<input type="hidden" name="authorId" value={authorId} />
<input name="title" placeholder="Titel" required />
<textarea name="content" placeholder="Inhalt" />
{state?.error && (
<p className="error">{JSON.stringify(state.error)}</p>
)}
<button type="submit" disabled={isPending}>
{isPending ? 'Speichern...' : 'Post erstellen'}
</button>
</form>
);
}
Der Aufruf von revalidatePath() nach jeder Mutation ist wichtig: Er invalidiert die gecachten React Server Component Outputs für den angegebenen Pfad. Wie Next.js das neue Caching-Modell im App Router verwaltet, erklärt unser Leitfaden zu Next.js Cache Components und der use cache-Direktive im Detail.
Connection Pooling für Serverless-Deployments
Auf Vercel laufen API-Routen und Server Actions als kurzlebige Serverless-Funktionen. Jeder Kaltstart öffnet eine neue Verbindung – bei hoher Last können so binnen Sekunden Hunderte simultaner Verbindungen entstehen. Das klingt dramatisch, ist aber ein echtes Problem, das ich in Produktion gesehen habe. Die Lösung: einen Pooler vorschalten.
Neon hat PgBouncer eingebaut. Im Dashboard findest du zwei Connection Strings – verwende sie situationsabhängig:
# .env (Produktion – Pooler für Runtime-Queries)
DATABASE_URL=postgresql://user:[email protected]/mydb?sslmode=require
# .env.migration (Direkte Verbindung ausschließlich für drizzle-kit migrate)
DATABASE_DIRECT_URL=postgresql://user:[email protected]/mydb?sslmode=require
Für Migrationen immer die direkte Verbindung verwenden – PgBouncer im Transaction-Pool-Modus unterstützt keine Prepared Statements, die drizzle-kit intern nutzt. Konfiguriere in drizzle.config.ts entsprechend DATABASE_DIRECT_URL für den Migrations-Workflow.
Drizzle Studio: Datenbank visuell verwalten
Drizzle Studio ist ein in drizzle-kit enthaltener visueller Datenbank-Browser. Starte ihn mit:
pnpm db:studio
Er öffnet sich automatisch unter https://local.drizzle.studio. Von dort kannst du Tabellendaten browsen, einzelne Zeilen bearbeiten, SQL-Queries absetzen, Relationen navigieren und Daten als CSV exportieren – alles ohne externes Tool wie TablePlus oder DBeaver. Studio liest deine drizzle.config.ts direkt und kennt damit dein komplettes Schema und alle Relations.
Kleiner Hinweis aus der Praxis: Safari und Brave blockieren localhost-Zugriffe von local.drizzle.studio standardmäßig. Nutze Chrome oder Firefox für den Studio-Zugriff, oder konfiguriere ein lokales TLS-Zertifikat mit mkcert. Das hat mich beim ersten Versuch ein paar Minuten Suche gekostet.
Häufige Fehler und wie du sie vermeidest
„Too many connections" im Development
Ursache: Kein Singleton, jeder Hot Reload öffnet eine neue Verbindung. Lösung: Den globalThis-Singleton aus dem Abschnitt Datenbankverbindung verwenden.
Relational Queries ergeben undefined für with-Felder
Ursache: Das schema-Objekt wurde nicht beim drizzle()-Aufruf übergeben. Lösung: drizzle({ client: sql, schema }) – das schema-Argument ist zwingend für Relational Queries und Drizzle Studio.
Migrationen schlagen auf Vercel fehl
Ursache: postinstall nutzt die gepoolte URL. PgBouncer blockiert die Prepared Statements von drizzle-kit. Lösung: Getrennte Environment Variables setzen – DATABASE_URL (Pooler) für Runtime, DATABASE_DIRECT_URL (direkt) für Migrationen – und drizzle.config.ts auf DATABASE_DIRECT_URL zeigen lassen.
TypeScript-Fehler: „Property does not exist on type"
Ursache: Ein veralteter Import oder ein Schema-Mismatch nach einem Schema-Update. Drizzle-Kit-Typen sind an das Schema-Objekt gebunden – nach jeder Schema-Änderung TypeScript-Server in der IDE neu starten (Cmd+Shift+P → TypeScript: Restart TS Server).
Häufig gestellte Fragen
Was ist der Unterschied zwischen Drizzle ORM und Prisma in Next.js?
Drizzle ORM ist ein leichtgewichtiges, serverless-optimiertes Library ohne Laufzeit-Codegenerierung – das Bundle ist unter 30 kB, verglichen mit mehreren Megabyte bei Prismas Query Engine. Drizzle schreibt typsichere Queries direkt als TypeScript, während Prisma eine eigene Schema-Sprache und einen generierten Client verwendet. Für Vercel-Deployments und Edge-Funktionen ist Drizzle 2026 die bevorzugte Wahl der Community.
Wie verhindere ich Connection Leaks bei Next.js Hot Reload?
Verwende den globalThis-Singleton-Pattern: Speichere die Datenbankinstanz in globalThis, da dieses Objekt Hot Reloads überlebt, während der Node.js-Prozess weiterläuft. In Production ist kein Singleton nötig, da Module dort einmalig durch den Module-Cache gehalten werden. Das vollständige Codebeispiel findest du im Abschnitt Datenbankverbindung dieses Artikels.
Kann ich Drizzle ORM mit dem Next.js Edge Runtime verwenden?
Ja, mit dem richtigen Treiber. Verwende @neondatabase/serverless mit dem HTTP-Adapter (drizzle-orm/neon-http) – dieser nutzt fetch statt Node.js-Socket-APIs und ist vollständig Edge-kompatibel. Der klassische pg-Treiber funktioniert nicht im Edge Runtime, da er Node.js-APIs wie net und tls benötigt.
Wann sollte ich drizzle-kit push statt migrate verwenden?
db:push eignet sich ausschließlich für die lokale Entwicklung bei schnellen Schema-Iterationen – es erstellt keine SQL-Dateien und ist nicht für Produktionsdatenbanken geeignet. Für Team-Projekte und Production-Deployments verwende immer db:generate + db:migrate: Die generierten SQL-Dateien in Git committen, so sind Schema-Änderungen nachvollziehbar und durch CI/CD automatisch eingespielt.
Wie inferiere ich TypeScript-Typen aus dem Drizzle-Schema?
Nutze die eingebauten Helfer-Properties direkt am Tabellen-Objekt: typeof users.$inferSelect ergibt den Typ einer gelesenen Zeile (alle Spalten inklusive generierten Werten), typeof users.$inferInsert den Typ für INSERT-Operationen (Spalten mit Default-Werten sind optional). Diese Typen aktualisieren sich automatisch bei Schema-Änderungen – kein manuelles Interface erforderlich.

