effidevFlutter · Cloudflare-Edge · Cloud-Kostenoptimierung
Deutsch

Drizzle ORM + Cloudflare D1: 99% kleineres Bundle als Prisma

Drizzle ORM und Cloudflare D1 Edge-SQLite-Architekturdiagramm

Warum ORMs auf dem Edge ein Problem sind

Haben Sie jemals Prisma auf Cloudflare Workers verwendet und extrem langsame Cold-Start-Zeiten erlebt? Der Grund dafür ist einfach: Die Bundle-Größe von Prisma beträgt 1.6 MB. Obwohl das Skriptgrößenlimit von Workers 10 MB beträgt, verdampfen durch das Laden eines 1.6 MB großen ORMs Hunderte von Millisekunden beim Cold Start.

Drizzle ORM wurde nach einer völlig entgegengesetzten Philosophie entwickelt. Die Bundle-Größe beträgt 12.2 KB (min+gzip) – das ist 99.2% kleiner im Vergleich zu Prisma. Es gibt keinen Codegenerierungsschritt und keine Rust-Engine. Es umschließt SQL mit reinem TypeScript. Daher funktioniert es in Edge-Runtimes ideal.

In diesem Artikel wird Schritt für Schritt erklärt, wie Sie durch die Kombination von Drizzle ORM und Cloudflare D1 eine vollständig typsichere Edge-Datenbankarchitektur aufbauen. Sie erstellen eine REST-API mit Hono, verwalten Migrationen mit drizzle-kit und richten eine lokale Entwicklungsumgebung ein.

Drizzle ORM vs. Prisma: Edge-Umgebungsvergleich

Beim Vergleich der beiden ORMs in einer realen Produktionsumgebung wird der Unterschied deutlich.

Kriterium Drizzle ORM Prisma 7+
Bundle-Größe (min+gzip) 12.2 KB 1.6 MB
Auswirkung auf Cold Start Minimal (~50-100ms) Gering (~80-150ms)
Edge-Runtime-Unterstützung First-Class Unterstützt ab v7
Codegenerierungsschritt Keiner Vorhanden (prisma generate)
Typinferenzmethode Direkt über TypeScript Generierter Client
SQL-Kontrollgrad Hoch (SQL-first) Mittel (Abstraktion)
Offizieller D1-Treiber drizzle-orm/d1 Keiner (Community)
Lernkurve Mittel Gering

Es trifft zu, dass Prisma 7 die Rust-Engine entfernt hat und auf reines TypeScript/WASM umgestiegen ist. Dennoch besteht bei der Bundle-Größe weiterhin ein 130-facher Unterschied. In einer Umgebung wie Workers, in der bei jeder Anfrage ein Cold Start auftreten kann, ist dieser Unterschied entscheidend.

Was ist Cloudflare D1?

D1 ist die serverlose SQLite-Datenbank von Cloudflare. Da der Zugriff direkt über Workers-Bindings erfolgt, entsteht kein zusätzlicher TCP-Verbindungs-Overhead. D1 unterstützt globale Lese-Replikate und erlaubt selbst im kostenlosen Tarif bis zu 5 Millionen Abfragen pro Tag.

Cloudflare Workers Anfrage


  D1-Binding (env.DB)
        │ (IPC im selben Rechenzentrum)

  SQLite-Datenbank
  (D1 Serverless-Instanz)

Der entscheidende Punkt ist, dass auf D1 über Bindings ohne HTTP-Anfragen zugegriffen wird. Die Latenz beträgt lediglich ein Zehntel einer externen Datenbankverbindung.

Projekt-Ersteinrichtung

1. Hono + Drizzle + D1 Scaffolding

# Hono Worker-Vorlage erstellen
npm create cloudflare@latest my-d1-app -- --template hono

cd my-d1-app

# Drizzle installieren
npm install drizzle-orm
npm install -D drizzle-kit

2. D1-Datenbank erstellen

# D1-Datenbank erstellen
npx wrangler d1 create my-app-db

# Ausgabebeispiel:
# [[d1_databases]]
# binding = "DB"
# database_name = "my-app-db"
# database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"

3. wrangler.jsonc Konfiguration

// wrangler.jsonc
{
  "name": "my-d1-app",
  "main": "src/index.ts",
  "compatibility_date": "2025-01-01",
  "compatibility_flags": ["nodejs_compat"],
  "d1_databases": [
    {
      "binding": "DB",
      "database_name": "my-app-db",
      "database_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "migrations_dir": "drizzle/migrations"
    }
  ]
}

4. drizzle.config.ts Konfiguration

// drizzle.config.ts
import { defineConfig } from "drizzle-kit";

export default defineConfig({
  dialect: "sqlite",
  schema: "./src/db/schema.ts",
  out: "./drizzle/migrations",
  driver: "d1-http",
  dbCredentials: {
    accountId: process.env.CLOUDFLARE_ACCOUNT_ID!,
    databaseId: process.env.CLOUDFLARE_DATABASE_ID!,
    token: process.env.CLOUDFLARE_D1_TOKEN!,
  },
});

Tipp für die lokale Entwicklung: Da lokal wrangler d1 migrations apply verwendet wird, dienen driver und dbCredentials in drizzle.config.ts ausschließlich für Remote-Migrationen in der Produktionsumgebung.

Schema-Definition: TypeScript-First

Die Kernstärke von Drizzle liegt darin, dass Schemadefinition und Typinferenz in einem Schritt vereint sind. Es ist kein separater Schritt wie prisma generate erforderlich.

// src/db/schema.ts
import {
  sqliteTable,
  text,
  integer,
  real,
  blob,
} from "drizzle-orm/sqlite-core";
import { sql } from "drizzle-orm";

// 유저 테이블
export const users = sqliteTable("users", {
  id: integer("id").primaryKey({ autoIncrement: true }),
  email: text("email").notNull().unique(),
  name: text("name").notNull(),
  role: text("role", { enum: ["admin", "user", "guest"] })
    .notNull()
    .default("user"),
  createdAt: text("created_at")
    .notNull()
    .default(sql`(datetime('now'))`),
  updatedAt: text("updated_at")
    .notNull()
    .default(sql`(datetime('now'))`),
});

// 포스트 테이블
export const posts = sqliteTable("posts", {
  id: integer("id").primaryKey({ autoIncrement: true }),
  title: text("title").notNull(),
  slug: text("slug").notNull().unique(),
  content: text("content").notNull(),
  authorId: integer("author_id")
    .notNull()
    .references(() => users.id, { onDelete: "cascade" }),
  published: integer("published", { mode: "boolean" }).default(false),
  viewCount: integer("view_count").default(0),
  publishedAt: text("published_at"),
  createdAt: text("created_at")
    .notNull()
    .default(sql`(datetime('now'))`),
});

// 태그 테이블 (M:N 관계)
export const tags = sqliteTable("tags", {
  id: integer("id").primaryKey({ autoIncrement: true }),
  name: text("name").notNull().unique(),
  slug: text("slug").notNull().unique(),
});

export const postTags = sqliteTable("post_tags", {
  postId: integer("post_id")
    .notNull()
    .references(() => posts.id, { onDelete: "cascade" }),
  tagId: integer("tag_id")
    .notNull()
    .references(() => tags.id, { onDelete: "cascade" }),
});

// 타입 추론 — 코드 생성 없이 즉시 사용 가능
export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;
export type Post = typeof posts.$inferSelect;
export type NewPost = typeof posts.$inferInsert;
export type Tag = typeof tags.$inferSelect;

Der entscheidende Unterschied zu Prisma: Sie müssen prisma generate nicht ausführen. Die Schemadatei selbst ist die Quelle der TypeScript-Typen. Die automatische Vervollständigung der IDE funktioniert sofort.

Migrations-Workflow

SQL-Migrationsdateien generieren

npx drizzle-kit generate

Dieser Befehl generiert SQL-Dateien im Ordner drizzle/migrations/:

-- drizzle/migrations/0000_initial.sql
CREATE TABLE `users` (
  `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
  `email` text NOT NULL,
  `name` text NOT NULL,
  `role` text DEFAULT 'user' NOT NULL,
  `created_at` text DEFAULT (datetime('now')) NOT NULL,
  `updated_at` text DEFAULT (datetime('now')) NOT NULL
);
--> statement-breakpoint
CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`);
--> statement-breakpoint
CREATE TABLE `posts` (
  `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
  `title` text NOT NULL,
  `slug` text NOT NULL,
  `content` text NOT NULL,
  `author_id` integer NOT NULL,
  `published` integer DEFAULT false,
  `view_count` integer DEFAULT 0,
  `published_at` text,
  `created_at` text DEFAULT (datetime('now')) NOT NULL,
  FOREIGN KEY (`author_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
);

Migrationen anwenden

# Auf lokales D1 anwenden (Entwicklung)
npx wrangler d1 migrations apply DB --local

# Auf Produktions-D1 anwenden
npx wrangler d1 migrations apply DB --remote

Wrangler verwaltet den Ausführungsverlauf über die Tabelle d1_migrations. Sie müssen sich keine Sorgen über doppelte Ausführungen machen.

Beispiel für Schemaänderungen

# Nach Schemaänderung neue Migration generieren
npx drizzle-kit generate

# Generierte Migration überprüfen und anwenden
npx wrangler d1 migrations apply DB --local

Workers + Hono Integration

DB-Client-Initialisierung

// src/db/client.ts
import { drizzle } from "drizzle-orm/d1";
import * as schema from "./schema";

export type Env = {
  DB: D1Database;
};

export function createDb(d1: D1Database) {
  return drizzle(d1, { schema });
}

export type DrizzleDb = ReturnType<typeof createDb>;

Hono-App-Konfiguration

// src/index.ts
import { Hono } from "hono";
import { createDb, type Env } from "./db/client";
import { usersRouter } from "./routes/users";
import { postsRouter } from "./routes/posts";

const app = new Hono<{ Bindings: Env }>();

// DB-Middleware
app.use("*", async (c, next) => {
  c.set("db", createDb(c.env.DB));
  await next();
});

app.route("/api/users", usersRouter);
app.route("/api/posts", postsRouter);

app.get("/health", (c) => c.json({ status: "ok" }));

export default app;

Users-Router — Vollständiges CRUD

// src/routes/users.ts
import { Hono } from "hono";
import { eq, like, desc, count } from "drizzle-orm";
import { users, posts, type NewUser } from "../db/schema";
import type { DrizzleDb, Env } from "../db/client";

const usersRouter = new Hono<{
  Bindings: Env;
  Variables: { db: DrizzleDb };
}>();

// GET /api/users — 페이지네이션 + 검색
usersRouter.get("/", async (c) => {
  const db = c.get("db");
  const { page = "1", limit = "20", q } = c.req.query();

  const pageNum = Math.max(1, parseInt(page));
  const limitNum = Math.min(100, parseInt(limit));
  const offset = (pageNum - 1) * limitNum;

  // WHERE 조건 동적 구성
  const whereClause = q ? like(users.name, `%${q}%`) : undefined;

  const [userList, totalResult] = await Promise.all([
    db
      .select()
      .from(users)
      .where(whereClause)
      .orderBy(desc(users.createdAt))
      .limit(limitNum)
      .offset(offset)
      .all(),
    db
      .select({ count: count() })
      .from(users)
      .where(whereClause)
      .get(),
  ]);

  return c.json({
    data: userList,
    pagination: {
      page: pageNum,
      limit: limitNum,
      total: totalResult?.count ?? 0,
      hasNext: offset + limitNum < (totalResult?.count ?? 0),
    },
  });
});

// GET /api/users/:id — 유저 + 포스트 수 조회
usersRouter.get("/:id", async (c) => {
  const db = c.get("db");
  const id = parseInt(c.req.param("id"));

  const user = await db
    .select({
      id: users.id,
      email: users.email,
      name: users.name,
      role: users.role,
      createdAt: users.createdAt,
      postCount: count(posts.id),
    })
    .from(users)
    .leftJoin(posts, eq(posts.authorId, users.id))
    .where(eq(users.id, id))
    .groupBy(users.id)
    .get();

  if (!user) {
    return c.json({ error: "User not found" }, 404);
  }

  return c.json(user);
});

// POST /api/users — 유저 생성
usersRouter.post("/", async (c) => {
  const db = c.get("db");
  const body = await c.req.json<NewUser>();

  // 이메일 중복 검사
  const existing = await db
    .select({ id: users.id })
    .from(users)
    .where(eq(users.email, body.email))
    .get();

  if (existing) {
    return c.json({ error: "Email already exists" }, 409);
  }

  const [newUser] = await db
    .insert(users)
    .values({
      email: body.email,
      name: body.name,
      role: body.role ?? "user",
    })
    .returning();

  return c.json(newUser, 201);
});

// PATCH /api/users/:id — 부분 업데이트
usersRouter.patch("/:id", async (c) => {
  const db = c.get("db");
  const id = parseInt(c.req.param("id"));
  const body = await c.req.json<Partial<NewUser>>();

  const [updated] = await db
    .update(users)
    .set({
      ...body,
      updatedAt: new Date().toISOString(),
    })
    .where(eq(users.id, id))
    .returning();

  if (!updated) {
    return c.json({ error: "User not found" }, 404);
  }

  return c.json(updated);
});

// DELETE /api/users/:id
usersRouter.delete("/:id", async (c) => {
  const db = c.get("db");
  const id = parseInt(c.req.param("id"));

  const [deleted] = await db
    .delete(users)
    .where(eq(users.id, id))
    .returning({ id: users.id });

  if (!deleted) {
    return c.json({ error: "User not found" }, 404);
  }

  return c.json({ message: "Deleted", id: deleted.id });
});

export { usersRouter };

Fortgeschrittene Abfragen: Beziehungsabfragen

// src/routes/posts.ts — 관계 데이터 JOIN
import { Hono } from "hono";
import { eq, and, desc, inArray } from "drizzle-orm";
import { posts, users, tags, postTags } from "../db/schema";
import type { DrizzleDb, Env } from "../db/client";

const postsRouter = new Hono<{
  Bindings: Env;
  Variables: { db: DrizzleDb };
}>();

// GET /api/posts/:slug — 포스트 상세 (유저 + 태그 포함)
postsRouter.get("/:slug", async (c) => {
  const db = c.get("db");
  const slug = c.req.param("slug");

  // 1. 포스트 + 작성자 조회
  const post = await db
    .select({
      id: posts.id,
      title: posts.title,
      slug: posts.slug,
      content: posts.content,
      published: posts.published,
      viewCount: posts.viewCount,
      publishedAt: posts.publishedAt,
      author: {
        id: users.id,
        name: users.name,
        email: users.email,
      },
    })
    .from(posts)
    .innerJoin(users, eq(posts.authorId, users.id))
    .where(and(eq(posts.slug, slug), eq(posts.published, true)))
    .get();

  if (!post) {
    return c.json({ error: "Post not found" }, 404);
  }

  // 2. 태그 조회 (별도 쿼리 — D1은 복잡한 서브쿼리보다 단순 쿼리 2개가 빠름)
  const postTagList = await db
    .select({ name: tags.name, slug: tags.slug })
    .from(tags)
    .innerJoin(postTags, eq(postTags.tagId, tags.id))
    .where(eq(postTags.postId, post.id))
    .all();

  // 3. 조회수 증가 (fire-and-forget)
  c.executionCtx.waitUntil(
    db
      .update(posts)
      .set({ viewCount: post.viewCount + 1 })
      .where(eq(posts.id, post.id))
      .run()
  );

  return c.json({ ...post, tags: postTagList });
});

export { postsRouter };

Das Inkrementieren von Aufrufzahlen mit c.executionCtx.waitUntil() ist ein wichtiges Entwurfsmuster für Workers. Die Datenbankaktualisierung wird auch nach dem Zurückgeben der Antwort im Hintergrund ausgeführt.

Lokale Entwicklung: Anbindung an lokales D1 SQLite

# Lokalen Entwicklungsserver starten
npx wrangler dev

# In einem separaten Terminal Migrationen anwenden
npx wrangler d1 migrations apply DB --local

Wrangler speichert den lokalen D1-Zustand als SQLite-Datei im Ordner .wrangler/state/v3/d1/. Selbst wenn der Entwicklungsserver neu gestartet wird, bleiben die Daten erhalten.

Lokale Datenbank direkt abfragen:

npx wrangler d1 execute DB --local --command "SELECT * FROM users LIMIT 5;"

Lokale Seed-Daten einfügen:

npx wrangler d1 execute DB --local --file ./drizzle/seed.sql

Transaktionen und Batch-Verarbeitung

Da D1 eine serverlose SQLite-Datenbank ist, unterstützt sie Transaktionen, die über Drizzle noch sicherer verwendet werden können.

// 트랜잭션: 유저 + 포스트 동시 생성
async function createUserWithPost(
  db: DrizzleDb,
  userData: NewUser,
  postData: Omit<NewPost, "authorId">
) {
  return await db.transaction(async (tx) => {
    const [user] = await tx
      .insert(users)
      .values(userData)
      .returning();

    const [post] = await tx
      .insert(posts)
      .values({ ...postData, authorId: user.id })
      .returning();

    return { user, post };
  });
}

Multi-Query-Optimierung mit der D1 Batch-API:

// D1 Batch API — 네트워크 왕복 1회로 여러 쿼리 실행
async function batchInsertPosts(db: DrizzleDb, postsData: NewPost[]) {
  // Drizzle의 batch 헬퍼 (D1 특화)
  const statements = postsData.map((post) =>
    db.insert(posts).values(post)
  );

  // 단일 HTTP 요청으로 전체 배치 처리
  await db.batch(statements);
}

db.batch() führt mehrere Abfragen mit einem einzigen Aufruf der HTTP-API von D1 aus. Wenn 1000 INSERT-Operationen einzeln ausgeführt werden, sind 1000 Netzwerk-Roundtrips erforderlich – bei der Batch-Verarbeitung nur ein einziger.

Kostenoptimierung: Betrieb innerhalb des kostenlosen D1-Tarifs

Grenzen des kostenlosen D1-Tarifs und Optimierungsmethoden mit Drizzle:

Kriterium Kostenlos Kostenpflichtig (Workers Paid)
Leseanfragen 5 Mio. pro Tag Unbegrenzt
Schreibanfragen 100.000 pro Tag Unbegrenzt
Speicherkapazität 5 GB 25 GB
Anzahl der Datenbanken 10 Unbegrenzt

Drizzle-Muster zur Reduzierung der Anzahl von Abfragen:

// ❌ N+1 쿼리 문제
const postList = await db.select().from(posts).all();
for (const post of postList) {
  const author = await db
    .select()
    .from(users)
    .where(eq(users.id, post.authorId))
    .get(); // 포스트 수만큼 쿼리 발생
}

// ✅ JOIN으로 1번에 해결
const postListWithAuthor = await db
  .select({
    postId: posts.id,
    title: posts.title,
    authorName: users.name,
  })
  .from(posts)
  .innerJoin(users, eq(posts.authorId, users.id))
  .all();
// ✅ 자주 읽는 데이터 → KV 캐싱으로 D1 읽기 요청 절감
export type CacheEnv = {
  DB: D1Database;
  KV: KVNamespace;
};

async function getPostWithCache(
  db: DrizzleDb,
  kv: KVNamespace,
  slug: string
) {
  const cacheKey = `post:${slug}`;

  // 캐시 히트
  const cached = await kv.get(cacheKey, "json");
  if (cached) return cached;

  // 캐시 미스 → D1 조회 후 캐싱
  const post = await db
    .select()
    .from(posts)
    .where(eq(posts.slug, slug))
    .get();

  if (post) {
    // 5분 TTL 캐싱
    await kv.put(cacheKey, JSON.stringify(post), { expirationTtl: 300 });
  }

  return post;
}

Der Einzelpreis für D1-Leseanfragen beträgt $0.001 pro 1 Million Abfragen. Wenn Sie häufig gelesene Daten im KV zwischenspeichern, können Sie die D1-Kosten um mehr als 90% senken.

Typsicherer API-Client: Kombination mit Hono RPC

Wenn Sie die Typen von Drizzle über Hono RPC bis zum Frontend weiterleiten, erreichen Sie echte End-to-End-Typsicherheit.

// src/index.ts — Hono RPC 타입 내보내기
import { Hono } from "hono";
import type { User, Post } from "./db/schema";

const app = new Hono<{ Bindings: Env }>();

const routes = app
  .get("/api/users", async (c) => {
    // ... 구현
    return c.json({ data: [] as User[] });
  })
  .post("/api/users", async (c) => {
    // ... 구현
    return c.json({} as User, 201);
  });

export type AppType = typeof routes;
export default app;
// 프론트엔드 (React / Next.js)
import { hc } from "hono/client";
import type { AppType } from "../worker/src/index";

const client = hc<AppType>("https://my-app.workers.dev");

// 완전한 타입 추론 — D1 스키마에서 프론트엔드까지 이어짐
const response = await client.api.users.$get();
const { data } = await response.json();
// data는 User[] 타입 — IDE 자동 완성 100% 작동

Typen werden automatisch weitergeleitet: Schemadefinition → Drizzle-Typen → Hono-Antwort → RPC-Client. Sie implementieren End-to-End-Typsicherheit rein mit TypeScript, ohne prisma generate, tRPC-Konfiguration oder GraphQL-Schemata.

CI/CD: GitHub Actions Deployment-Pipeline

# .github/workflows/deploy.yml
name: Deploy to Cloudflare Workers

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "npm"

      - name: Install dependencies
        run: npm ci

      - name: TypeScript 타입 검사
        run: npx tsc --noEmit

      - name: D1 마이그레이션 적용 (프로덕션)
        env:
          CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
        run: npx wrangler d1 migrations apply DB --remote

      - name: Workers 배포
        env:
          CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
        run: npx wrangler deploy

Die Reihenfolge Migration → Deployment ist entscheidend. Sie müssen die Migration zuerst anwenden, damit der neue Workers-Code auf das neue Schema verweisen kann.

Drizzle Studio: Datenbankvisualisierung ohne GUI

# Drizzle Studio mit Anbindung an lokales D1 ausführen
npx drizzle-kit studio

Wenn Sie https://local.drizzle.studio im Browser öffnen, können Sie D1-Daten über eine grafische Benutzeroberfläche anzeigen und bearbeiten. Wenn Sie an Prisma Studio gewöhnt sind, ist es genau dieselbe Erfahrung.

Performance-Benchmark: Reale D1-Latenz

Tatsächliche Messwerte bei der Verwendung von D1 in Workers (basierend auf der offiziellen Cloudflare-Dokumentation):

Abfragetyp Durchschnittliche Latenz
Einfaches SELECT (Index) 1-5ms
JOIN-Abfrage 5-15ms
INSERT 5-10ms
Komplexe Aggregationsabfrage 10-50ms
Batch-INSERT (100 Einträge) 10-20ms

Im Vergleich zu externen PostgreSQL-Datenbanken (Neon, Supabase usw.) erfolgt der D1-Binding-Aufruf innerhalb von Workers ohne TCP-Verbindung, wodurch die Latenz um das 3- bis 5-Fache geringer ist. Wenn Ihr Ziel ein TTFB unter 100 ms ist, ist D1 die beste Wahl.

D1 vs. andere Edge-Datenbanken: Auswahlkriterien

Szenario Empfehlung
Cloudflare All-in-One Stack D1 + Drizzle
Multi-Cloud (Vercel + CF gemischt) Turso + Drizzle
PostgreSQL-Funktionen benötigt (JSONB, Volltext) Neon + Drizzle
Echtzeit-Synchronisierung benötigt Supabase + Drizzle
Komplexe Analyseabfragen Externe PostgreSQL

Wenn Sie sich ausschließlich im Cloudflare-Ökosystem befinden, ist D1 mit Abstand die beste Lösung. Wenn Sie Multi-Cloud benötigen, ist Turso auf libSQL-Basis portabel und nutzt dieselbe Drizzle-API wie D1.

Migrations-Checkliste

Prüfpunkte beim Umstieg von Prisma auf Drizzle:

Der Umstieg mag aufwendig erscheinen, aber sobald Sie mit Drizzle vertraut sind, fühlt sich die direkte Kontrolle über SQL viel intuitiver an. Es basiert auf der Philosophie, dass ein ORM SQL nicht verbirgt, sondern SQL in TypeScript ausdrückt.

Fazit

Die Kombination aus Drizzle ORM und Cloudflare D1 ist eine der effizientesten Optionen für den Edge-Full-Stack im Jahr 2026.

Wenn das 1.6 MB große Bundle von Prisma in Ihrer Workers-Umgebung zu Problemen geführt hat, ist der Umstieg auf Drizzle nicht nur eine Frage der Bundle-Größe. Es ist auch ein philosophischer Wandel: Anstatt ein ORM als magische Blackbox zu verwenden, drücken Sie SQL in TypeScript aus.

Verwandter Artikel: Im Leitfaden Hono RPC & Cloudflare Workers: TanStack Query v5 Typsicherheit finden Sie ein vollständiges Full-Stack-Beispiel, das die Drizzle-API mit Hono RPC kombiniert.