Claude Code Drizzle ORM: Type-safe SQL, Schema-First Design und moderne Datenbankentwicklung mit KI
Datenbankcode ist der Teil eines Projekts, der am häufigsten still kaputt geht. Ein falscher Typ hier, ein nicht gemigtes Schema dort, eine Query die zur Laufzeit knallt weil das Feld umbenannt wurde — und alles sah im Editor noch grün aus. Drizzle ORM löst genau dieses Problem: Type-safety vom Schema bis zur Query, SQL-Nähe ohne ORM-Magie, und ein Entwicklungsgefühl, das sich wie TypeScript anfühlt statt wie ein Framework das TypeScript toleriert.
Claude Code und Drizzle ORM passen dabei außergewöhnlich gut zusammen. Nicht weil Claude Code irgendwelche Drizzle-spezifischen Tricks kennt, sondern weil Drizzle's Schema-First-Ansatz für Claude Code transparent ist: das Schema ist TypeScript-Code, die Queries sind TypeScript-Code, die Migrations sind SQL. Kein Abstraktionsberg, der den Kontext verbirgt. Claude Code liest das Schema, versteht die Relationen, schreibt Queries die tatsächlich kompilieren.
Dieser Artikel zeigt, wie beides zusammen in der Praxis funktioniert — von der Ersteinrichtung über Schema-Design und Migrations bis zu komplexen Queries und Drizzle Studio.
Claude Code Mastery — Datenbankentwicklung, Agents und Workflows auf Deutsch
Drizzle ORM ist ein Thema im Kurs — zusammen mit TypeScript, Next.js, MCP-Servern und autonomen Agents. Einmalig bezahlt, kein Abo, vollständig auf Deutsch.
Zum Kurs — Jetzt starten → Einmalzahlung · Kein Abo · 14 Tage Rückgaberecht1. Warum Drizzle ORM — und warum jetzt
Prisma war lange Zeit die Standardwahl für TypeScript-Projekte, die eine typisierte Datenbankschicht wollten. Das Problem: Prisma abstrahiert SQL so stark, dass man schnell in Situationen landet, wo man nicht mehr weiß, welche SQL-Queries tatsächlich ausgeführt werden. Und wenn die Performance-Analyse zeigt, dass ein Query 400ms statt 4ms braucht, fängt die Debugging-Session in einer Abstraktionsschicht an, die einem im Weg steht.
Drizzle geht den entgegengesetzten Weg: SQL first, TypeScript second. Du schreibst TypeScript-Code, der eins zu eins auf SQL-Queries gemapped wird. Keine ORM-Magie, keine N+1-Überraschungen, kein "das Prisma-Modell verhält sich hier etwas anders als erwartet". Was du siehst ist was du bekommst — und was du bekommst ist valides SQL.
Die Konsequenz für Claude Code: Wenn du Claude Code bittest, eine Query zu schreiben, hat es genau das gleiche Modell im Kopf wie du. Das Schema ist TypeScript, die Query ist TypeScript, der Typ-Check ist TypeScript. Claude Code kann die Query schreiben, den Typ-Fehler finden und die Migration generieren — alles ohne eigene Abstraktionsebene zwischen sich und dem eigentlichen Problem.
Drizzle ORM unterstützt: PostgreSQL, MySQL, SQLite (und SQLite-kompatible wie Cloudflare D1, Turso, libSQL). Die Beispiele in diesem Artikel verwenden vorwiegend PostgreSQL und SQLite — die Konzepte sind identisch, nur der Treiber unterscheidet sich.
2. Einrichtung: Schema, Config, Treiber
Drizzle braucht drei Dinge: das Schema, die Drizzle-Config für Migrations, und den Datenbank-Treiber. Claude Code kann alle drei generieren, wenn man es mit der Projektstruktur vertraut macht:
claude "Richte Drizzle ORM für ein Next.js-Projekt mit PostgreSQL ein.
Installiere die nötigen Pakete, erstelle drizzle.config.ts, einen
initialen Schema-File unter src/db/schema.ts und eine db.ts für
die Connection. Nutze postgres (node-postgres) als Treiber."
Claude Code installiert drizzle-orm, drizzle-kit und postgres, erstellt die Konfiguration und schreibt eine funktionierende Basis. Das Ergebnis sieht typischerweise so aus:
// drizzle.config.ts
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
schema: './src/db/schema.ts',
out: './drizzle',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL!,
},
});
// src/db/db.ts
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as schema from './schema';
const client = postgres(process.env.DATABASE_URL!);
export const db = drizzle(client, { schema });
Zwei Punkte, die Claude Code dabei korrekt handhabt: Das Schema wird als Import an drizzle() übergeben — das ist notwendig für Relations und für den Typ-Inferenz-Mechanismus von Drizzle. Und die Connection wird einmal erstellt und exportiert, nicht bei jedem Request neu instanziiert.
SQLite-Variante für lokale Entwicklung
Für Projekte die SQLite nutzen (Turso, Cloudflare D1, oder einfach lokale Entwicklung) ist die Einrichtung ähnlich, aber mit anderem Treiber:
// src/db/db.ts (SQLite / Turso)
import { drizzle } from 'drizzle-orm/libsql';
import { createClient } from '@libsql/client';
const client = createClient({
url: process.env.DATABASE_URL!,
authToken: process.env.DATABASE_AUTH_TOKEN,
});
export const db = drizzle(client);
3. Schema-First Design mit Claude Code
Das Herzstück von Drizzle ist das Schema. Hier definierst du deine Tabellen, Spalten, Typen und Constraints als TypeScript-Code. Claude Code ist bei diesem Schritt besonders nützlich — nicht weil es Drizzle-Syntax auswendig kennt, sondern weil es von einer Beschreibung auf ein vollständiges, valides Schema schließen kann, das alle Constraints und Relationen korrekt abbildet.
claude "Erstelle ein Drizzle-Schema für eine SaaS-Anwendung mit:
- users (id, email, name, createdAt, role: 'admin' | 'user')
- organizations (id, name, slug, plan: 'free' | 'pro' | 'enterprise')
- memberships (userId, organizationId, joinedAt)
- projects (id, orgId, name, description, createdAt, updatedAt)
Nutze PostgreSQL-Dialekt mit UUID-PKs und korrekten Foreign-Key-Constraints."
Ein solches Schema sieht dann so aus:
// src/db/schema.ts
import {
pgTable, uuid, text, timestamp, pgEnum,
uniqueIndex, primaryKey
} from 'drizzle-orm/pg-core';
export const roleEnum = pgEnum('role', ['admin', 'user']);
export const planEnum = pgEnum('plan', ['free', 'pro', 'enterprise']);
export const users = pgTable('users', {
id: uuid('id').defaultRandom().primaryKey(),
email: text('email').notNull().unique(),
name: text('name').notNull(),
role: roleEnum('role').notNull().default('user'),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
export const organizations = pgTable('organizations', {
id: uuid('id').defaultRandom().primaryKey(),
name: text('name').notNull(),
slug: text('slug').notNull().unique(),
plan: planEnum('plan').notNull().default('free'),
});
export const memberships = pgTable('memberships', {
userId: uuid('user_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
organizationId: uuid('organization_id')
.notNull()
.references(() => organizations.id, { onDelete: 'cascade' }),
joinedAt: timestamp('joined_at').defaultNow().notNull(),
}, (t) => ({
pk: primaryKey({ columns: [t.userId, t.organizationId] }),
}));
export const projects = pgTable('projects', {
id: uuid('id').defaultRandom().primaryKey(),
orgId: uuid('org_id')
.notNull()
.references(() => organizations.id, { onDelete: 'cascade' }),
name: text('name').notNull(),
description: text('description'),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
});
Was Claude Code dabei korrekt umsetzt: Enums als pgEnum, Composite Primary Keys für die Membership-Tabelle, onDelete: 'cascade' für Foreign Keys, und defaultRandom() für UUID-PKs. Das sind keine Selbstverständlichkeiten — viele automatisch generierte Schemas vergessen Foreign-Key-Constraints oder verwenden String-Typen statt nativer Postgres-Enums.
4. Relations definieren
Drizzle trennt bewusst zwischen dem Datenbank-Schema (Tabellen, Spalten, Constraints) und den Relations (wie Drizzle die Tabellen für Queries verbindet). Das ist zunächst ungewohnt, macht aber das System transparenter: Foreign Keys sind Datenbank-Constraints, Relations sind Query-Hilfen.
// src/db/relations.ts
import { relations } from 'drizzle-orm';
import { users, organizations, memberships, projects } from './schema';
export const usersRelations = relations(users, ({ many }) => ({
memberships: many(memberships),
}));
export const organizationsRelations = relations(organizations, ({ many }) => ({
memberships: many(memberships),
projects: many(projects),
}));
export const membershipsRelations = relations(memberships, ({ one }) => ({
user: one(users, {
fields: [memberships.userId],
references: [users.id],
}),
organization: one(organizations, {
fields: [memberships.organizationId],
references: [organizations.id],
}),
}));
export const projectsRelations = relations(projects, ({ one }) => ({
organization: one(organizations, {
fields: [projects.orgId],
references: [organizations.id],
}),
}));
Claude Code generiert diesen Teil auf Anfrage — man muss nur das Schema mitgeben und beschreiben, welche Relationen man abbilden möchte. Besonders nützlich ist Claude Code dabei, wenn Relations komplexer werden: Many-to-Many über Junction-Tables, Self-References (z.B. ein User der einen Manager hat, der selbst ein User ist), oder polymorphe Relationen.
5. Migrations generieren und ausführen
Drizzle Kit generiert SQL-Migrations aus der Differenz zwischen dem aktuellen Schema und dem letzten Migrations-Stand. Das ist einer der stärksten Punkte von Drizzle: die generierten SQL-Dateien sind lesbar, versionierbar und können manuell angepasst werden.
# Migration generieren
npx drizzle-kit generate
# Migration ausführen
npx drizzle-kit migrate
Die generierte SQL-Datei sieht dann ungefähr so aus:
-- drizzle/0000_initial.sql
CREATE TYPE "public"."role" AS ENUM('admin', 'user');
CREATE TYPE "public"."plan" AS ENUM('free', 'pro', 'enterprise');
CREATE TABLE IF NOT EXISTS "users" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"email" text NOT NULL,
"name" text NOT NULL,
"role" "role" DEFAULT 'user' NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL
);
ALTER TABLE "users" ADD CONSTRAINT "users_email_unique" UNIQUE("email");
CREATE TABLE IF NOT EXISTS "organizations" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"name" text NOT NULL,
"slug" text NOT NULL,
"plan" "plan" DEFAULT 'free' NOT NULL
);
-- ... weitere Tabellen
Claude Code kann diesen Prozess begleiten und erklären, was eine Migration tut und ob sie sicher in Production ausgeführt werden kann. Besonders relevant bei großen Tabellen: Spalten hinzufügen ist meist sicher, Spalten umbenennen oder Typen ändern kann unter Last problematisch sein.
Migrations in Production: Bevor du eine Migration gegen eine Produktionsdatenbank ausführst, frag Claude Code explizit: "Welche Operationen in dieser Migration könnten unter Last Probleme verursachen?" Spalten-Umbenennungen, NOT NULL ohne DEFAULT, und große Indizes sind typische Kandidaten für Downtime.
6. Queries: von einfach bis komplex
Drizzle bietet zwei Query-APIs: die Query-Builder-API (die SQL widerspiegelt) und die Relational Query API (die auf den definierten Relations aufbaut). Beide haben ihre Stärken — Claude Code nutzt situativ die passende.
Query Builder API
// Einfache Select-Query
const allUsers = await db.select().from(users);
// Mit Conditions
const adminUsers = await db
.select()
.from(users)
.where(eq(users.role, 'admin'));
// Mit Join
const membersWithOrgs = await db
.select({
userName: users.name,
userEmail: users.email,
orgName: organizations.name,
orgPlan: organizations.plan,
})
.from(memberships)
.innerJoin(users, eq(memberships.userId, users.id))
.innerJoin(organizations, eq(memberships.organizationId, organizations.id));
// Insert mit Rückgabe
const [newUser] = await db
.insert(users)
.values({
email: 'max@example.com',
name: 'Max Mustermann',
role: 'user',
})
.returning();
Relational Query API
Die Relational API ist für verschachtelte Daten gedacht und liest sich natürlicher wenn man alle Daten einer Entity inkl. Relations laden möchte:
// Organization mit allen Memberships und deren Users laden
const orgWithMembers = await db.query.organizations.findFirst({
where: eq(organizations.slug, 'my-org'),
with: {
memberships: {
with: {
user: {
columns: {
id: true,
name: true,
email: true,
},
},
},
},
projects: true,
},
});
Claude Code schreibt solche Queries auf Beschreibung hin zuverlässig korrekt — sowohl syntaktisch als auch semantisch. Man muss nicht selbst überlegen, ob ein leftJoin oder innerJoin die richtigen Rows zurückgibt; Claude Code erklärt den Unterschied und wählt die passende Variante.
Komplexere Queries: Aggregationen und Subqueries
claude "Schreib eine Drizzle-Query die für jede Organization die
Anzahl der Members und die Anzahl der Projects zurückgibt,
sortiert nach Member-Anzahl absteigend."
Claude Code generiert daraus eine saubere Aggregations-Query mit count(), groupBy() und orderBy():
import { count, desc } from 'drizzle-orm';
const orgsWithCounts = await db
.select({
orgId: organizations.id,
orgName: organizations.name,
memberCount: count(memberships.userId).as('member_count'),
})
.from(organizations)
.leftJoin(memberships, eq(organizations.id, memberships.organizationId))
.groupBy(organizations.id, organizations.name)
.orderBy(desc(count(memberships.userId)));
7. Type Inference — der eigentliche Vorteil
Was Drizzle von anderen ORMs unterscheidet ist nicht die Syntax, sondern die vollständige Typ-Inferenz. Jede Query gibt einen exakt typisierten Return-Typ zurück — abgeleitet direkt aus dem Schema, ohne manuelle Type-Deklarationen.
// Drizzle inferiert den Typ automatisch
type User = typeof users.$inferSelect;
// {
// id: string;
// email: string;
// name: string;
// role: 'admin' | 'user';
// createdAt: Date;
// }
type NewUser = typeof users.$inferInsert;
// {
// id?: string;
// email: string;
// name: string;
// role?: 'admin' | 'user';
// createdAt?: Date;
// }
Diese Typen kannst du direkt als Return-Typen für Funktionen, API-Responses oder Props verwenden. Wenn du eine Spalte im Schema umbenennst, ändert sich der Typ automatisch — und TypeScript zeigt dir sofort alle Stellen im Code, die angepasst werden müssen. Claude Code nutzt diese Inferenz: wenn es auf Basis deines Schemas Queries schreibt, stimmen die Typen, weil es das Schema liest und ableitet.
Das ist der eigentliche Produktivitätsgewinn: nicht nur dass Claude Code die Query schreibt, sondern dass es Code schreibt der bei Schemaänderungen automatisch Typ-Fehler produziert statt zur Laufzeit zu knallen.
8. Drizzle Studio — visuelles Datenbankmanagement
Drizzle Studio ist ein lokales, browser-basiertes Tool das deine Drizzle-Schema-Datenbank visuell darstellt und direkte Abfragen und Bearbeitungen ermöglicht. Starten mit einem einzigen Befehl:
npx drizzle-kit studio
Was Drizzle Studio bietet:
- Tabellenansicht: Alle Tabellen mit ihren Daten, paginiert und filterbar
- Inline-Editing: Daten direkt in der Tabellenansicht bearbeiten
- SQL-Editor: Beliebige SQL-Queries ausführen
- Schema-Ansicht: Alle Tabellen, Spalten und Constraints auf einen Blick
- Relations-Visualisierung: Entity-Relationship-Diagramm der definierten Relations
Für die Entwicklung ist Drizzle Studio das Äquivalent zu pgAdmin oder TablePlus — nur direkt in den Drizzle-Workflow integriert und ohne Konfigurationsaufwand. Claude Code und Drizzle Studio ergänzen sich: Claude Code schreibt den Code, Drizzle Studio zeigt das Ergebnis visuell.
Drizzle Studio in CI: Drizzle Studio ist ein lokales Entwicklungstool, kein Produktions-Interface. In CI/CD nutzt man drizzle-kit migrate direkt, nicht Studio. Studio öffnet standardmäßig auf Port 4983.
9. Seed-Daten und Testdaten mit Claude Code
Ein oft unterschätzter Aufwand: Testdaten für eine neue Datenbankstruktur erstellen. Mit Drizzle und Claude Code geht das schnell:
claude "Schreib ein Seed-Script für unsere Datenbank das:
- 3 Organizations erstellt (free, pro, enterprise)
- Je 5 Users pro Organization
- 2-3 Projects pro Organization
Nutze unsere bestehende db.ts und das Schema aus schema.ts."
Claude Code liest das Schema, versteht die Constraints (z.B. unique email, Foreign Keys) und schreibt ein Seed-Script das alle Constraints respektiert und die Daten in der richtigen Reihenfolge einfügt:
// src/db/seed.ts
import { db } from './db';
import { users, organizations, memberships, projects } from './schema';
async function seed() {
console.log('Seeding database...');
// Organizations in der richtigen Reihenfolge
const [freeOrg, proOrg, enterpriseOrg] = await db
.insert(organizations)
.values([
{ name: 'Free Tier Corp', slug: 'free-tier-corp', plan: 'free' },
{ name: 'Pro Solutions GmbH', slug: 'pro-solutions', plan: 'pro' },
{ name: 'Enterprise AG', slug: 'enterprise-ag', plan: 'enterprise' },
])
.returning();
// Users erstellen
const createdUsers = await db
.insert(users)
.values(
[freeOrg, proOrg, enterpriseOrg].flatMap((org, orgIndex) =>
Array.from({ length: 5 }, (_, i) => ({
email: `user${orgIndex * 5 + i + 1}@example.com`,
name: `User ${orgIndex * 5 + i + 1}`,
role: i === 0 ? ('admin' as const) : ('user' as const),
}))
)
)
.returning();
// Memberships verknüpfen
// ... (Claude Code generiert den vollständigen Code)
console.log('Seeding complete.');
}
seed().catch(console.error);
10. Drizzle in Kombination mit Next.js Server Actions
Drizzle ist besonders stark in Next.js 14+ Projekten mit Server Actions, weil die Datenbankzugriffe direkt im Server-Kontext passieren — kein separates API-Layer nötig für einfache Operationen:
// app/actions/projects.ts
'use server';
import { db } from '@/db/db';
import { projects } from '@/db/schema';
import { eq } from 'drizzle-orm';
import { revalidatePath } from 'next/cache';
export async function createProject(orgId: string, name: string) {
const [project] = await db
.insert(projects)
.values({ orgId, name })
.returning();
revalidatePath('/dashboard');
return project;
}
export async function deleteProject(projectId: string) {
await db.delete(projects).where(eq(projects.id, projectId));
revalidatePath('/dashboard');
}
claude "Erstelle Server Actions für Project-CRUD in unserem
Next.js-Projekt. Nutze Drizzle für alle Datenbankoperationen,
validiere Inputs mit Zod, und gib typisierte Ergebnisse zurück."
Claude Code schreibt dabei vollständige Actions inklusive Zod-Validierung, Error-Handling und revalidatePath() am richtigen Ort — weil es sowohl das Schema als auch die Next.js-Konventionen kennt.
11. Häufige Fehler und wie Claude Code dabei hilft
Einige Fehler tauchen in Drizzle-Projekten immer wieder auf. Claude Code erkennt und löst sie zuverlässig:
Missing from import
// Fehler: 'eq' is not defined
const user = await db.select().from(users).where(eq(users.id, id));
// Fix: Import ergänzen
import { eq } from 'drizzle-orm';
Schema nicht in db.ts übergeben
Wenn die Relational Query API (db.query.users.findMany()) nicht funktioniert, liegt es fast immer daran, dass das Schema nicht an drizzle() übergeben wurde:
// Falsch:
export const db = drizzle(client);
// Richtig:
import * as schema from './schema';
export const db = drizzle(client, { schema });
Relations in separater Datei, aber nicht importiert
Relations müssen in der Schema-Datei oder einer Datei sein, die in das Schema-Export eingebunden ist. Claude Code prüft das automatisch wenn es Relations-Queries generiert.
12. Performance: Drizzle vs. Raw SQL
Ein häufiges Argument gegen ORMs ist Performance. Bei Drizzle ist dieses Argument schwächer als bei klassischen ORMs, weil Drizzle die SQL-Queries nicht versteckt. Trotzdem gibt es Szenarien wo Raw SQL sinnvoller ist:
// Drizzle's sql-Template für komplexe Queries
import { sql } from 'drizzle-orm';
const result = await db.execute(sql`
WITH ranked_members AS (
SELECT
user_id,
organization_id,
ROW_NUMBER() OVER (
PARTITION BY organization_id
ORDER BY joined_at
) as rank
FROM memberships
)
SELECT * FROM ranked_members WHERE rank = 1
`);
Claude Code weiß, wann db.execute(sql\`...\`) die bessere Wahl ist als der Query Builder — bei Window Functions, komplexen CTEs, oder datenbankspezifischen Features die Drizzle noch nicht nativ unterstützt. Und es schreibt das SQL, das in dem Template landet, korrekt und ohne Injection-Risiken.
Claude Code Mastery — von Drizzle ORM bis zu autonomen Agents
Drizzle ORM, TypeScript, Next.js, MCP-Server, Hooks, Multi-Agent-Workflows — vollständig auf Deutsch, einmalig bezahlt, kein Abo.
Jetzt starten → Einmalzahlung · Kein Abo · 14 Tage RückgaberechtKurs · Claude Code Mastery
Von Drizzle ORM zum produktiven AI-Agenten
Datenbankentwicklung. Agents. MCP. Hooks. Alles auf Deutsch, einmalig bezahlt — kein Abo, keine Plattformabhängigkeit.
Einmalzahlung · Kein Abo · 14 Tage Rückgaberecht
Verwandte Artikel aus der Serie:
- Claude Code Debugging — Bugs in Minuten statt Stunden finden
- Claude Code für Python-Projekte — Claude Code in Python-Workflows einsetzen
- Claude Code für Unternehmen — Deployment, Zugriffskontrolle und Team-Workflows