Files
gh-jezweb-claude-skills-ski…/references/drizzle-integration.md
2025-11-30 08:24:16 +08:00

12 KiB

Drizzle ORM Integration Guide

Complete guide to using Drizzle ORM with Cloudflare Hyperdrive.


Overview

Drizzle ORM is a lightweight, TypeScript-first ORM with excellent type safety and performance.

Why Drizzle + Hyperdrive?

  • Type-safe queries with full TypeScript support
  • Zero runtime overhead (SQL is generated at build time)
  • Works with both PostgreSQL and MySQL via Hyperdrive
  • Simpler than Prisma (no code generation step in Worker)
  • Better performance than traditional ORMs

Installation

PostgreSQL

# Drizzle ORM + postgres.js driver
npm install drizzle-orm postgres

# Dev dependencies
npm install -D drizzle-kit @types/node

MySQL

# Drizzle ORM + mysql2 driver
npm install drizzle-orm mysql2

# Dev dependencies
npm install -D drizzle-kit @types/node

PostgreSQL Setup

1. Define Schema

Create src/db/schema.ts:

import { pgTable, serial, varchar, text, timestamp, boolean } from "drizzle-orm/pg-core";

export const users = pgTable("users", {
  id: serial("id").primaryKey(),
  name: varchar("name", { length: 255 }).notNull(),
  email: varchar("email", { length: 255 }).notNull().unique(),
  createdAt: timestamp("created_at").defaultNow(),
  updatedAt: timestamp("updated_at").defaultNow(),
});

export const posts = pgTable("posts", {
  id: serial("id").primaryKey(),
  title: varchar("title", { length: 255 }).notNull(),
  content: text("content"),
  published: boolean("published").default(false),
  authorId: serial("author_id").references(() => users.id),
  createdAt: timestamp("created_at").defaultNow(),
});

2. Use in Worker

import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import { users, posts } from "./db/schema";
import { eq } from "drizzle-orm";

type Bindings = {
  HYPERDRIVE: Hyperdrive;
};

export default {
  async fetch(request: Request, env: Bindings, ctx: ExecutionContext) {
    // Create postgres.js connection
    const sql = postgres(env.HYPERDRIVE.connectionString, {
      max: 5,
      prepare: true,  // CRITICAL for caching
      fetch_types: false  // Disable if not using array types
    });

    // Create Drizzle client
    const db = drizzle(sql);

    try {
      // INSERT
      const [newUser] = await db.insert(users).values({
        name: "John Doe",
        email: `john.${Date.now()}@example.com`
      }).returning();

      // SELECT
      const allUsers = await db.select().from(users);

      // WHERE
      const user = await db.select()
        .from(users)
        .where(eq(users.email, "john@example.com"));

      // JOIN
      const usersWithPosts = await db.select()
        .from(users)
        .leftJoin(posts, eq(users.id, posts.authorId));

      // UPDATE
      await db.update(users)
        .set({ name: "Jane Doe" })
        .where(eq(users.id, newUser.id));

      // DELETE
      // await db.delete(users).where(eq(users.id, 123));

      return Response.json({
        newUser,
        allUsers,
        user,
        usersWithPosts
      });
    } finally {
      ctx.waitUntil(sql.end());
    }
  }
};

3. Configure Drizzle Kit (Migrations)

Create drizzle.config.ts in project root:

import 'dotenv/config';
import { defineConfig } from 'drizzle-kit';

export default defineConfig({
  out: './drizzle',
  schema: './src/db/schema.ts',
  dialect: 'postgresql',
  dbCredentials: {
    url: process.env.DATABASE_URL!,
  },
});

Create .env file (for migrations only, NOT used in Worker):

# Direct connection to database (for migrations)
DATABASE_URL="postgres://user:password@host:5432/database"

4. Run Migrations

# Generate migration files from schema
npx drizzle-kit generate

# Apply migrations to database
npx drizzle-kit migrate

# Push schema directly (no migration files)
npx drizzle-kit push

Generated SQL (in drizzle/ folder):

-- drizzle/0000_initial.sql
CREATE TABLE IF NOT EXISTS "users" (
  "id" serial PRIMARY KEY NOT NULL,
  "name" varchar(255) NOT NULL,
  "email" varchar(255) NOT NULL,
  "created_at" timestamp DEFAULT now(),
  "updated_at" timestamp DEFAULT now(),
  CONSTRAINT "users_email_unique" UNIQUE("email")
);

CREATE TABLE IF NOT EXISTS "posts" (
  "id" serial PRIMARY KEY NOT NULL,
  "title" varchar(255) NOT NULL,
  "content" text,
  "published" boolean DEFAULT false,
  "author_id" serial NOT NULL,
  "created_at" timestamp DEFAULT now()
);

ALTER TABLE "posts" ADD CONSTRAINT "posts_author_id_users_id_fk"
  FOREIGN KEY ("author_id") REFERENCES "users"("id");

MySQL Setup

1. Define Schema

Create src/db/schema.ts:

import { mysqlTable, int, varchar, text, timestamp, boolean } from "drizzle-orm/mysql-core";

export const users = mysqlTable("users", {
  id: int("id").primaryKey().autoincrement(),
  name: varchar("name", { length: 255 }).notNull(),
  email: varchar("email", { length: 255 }).notNull(),
  createdAt: timestamp("created_at").defaultNow(),
  updatedAt: timestamp("updated_at").defaultNow(),
});

export const posts = mysqlTable("posts", {
  id: int("id").primaryKey().autoincrement(),
  title: varchar("title", { length: 255 }).notNull(),
  content: text("content"),
  published: boolean("published").default(false),
  authorId: int("author_id").references(() => users.id),
  createdAt: timestamp("created_at").defaultNow(),
});

2. Use in Worker

import { drizzle } from "drizzle-orm/mysql2";
import { createConnection } from "mysql2";
import { users, posts } from "./db/schema";
import { eq } from "drizzle-orm";

type Bindings = {
  HYPERDRIVE: Hyperdrive;
};

export default {
  async fetch(request: Request, env: Bindings, ctx: ExecutionContext) {
    // Create mysql2 connection
    const connection = createConnection({
      host: env.HYPERDRIVE.host,
      user: env.HYPERDRIVE.user,
      password: env.HYPERDRIVE.password,
      database: env.HYPERDRIVE.database,
      port: env.HYPERDRIVE.port,
      disableEval: true  // REQUIRED for Workers
    });

    // Create Drizzle client
    const db = drizzle(connection);

    try {
      // INSERT
      await db.insert(users).values({
        name: "John Doe",
        email: `john.${Date.now()}@example.com`
      });

      // SELECT
      const allUsers = await db.select().from(users);

      // WHERE
      const user = await db.select()
        .from(users)
        .where(eq(users.id, 1));

      return Response.json({ allUsers, user });
    } finally {
      ctx.waitUntil(
        new Promise<void>((resolve) => {
          connection.end(() => resolve());
        })
      );
    }
  }
};

3. Configure Drizzle Kit (MySQL)

import 'dotenv/config';
import { defineConfig } from 'drizzle-kit';

export default defineConfig({
  out: './drizzle',
  schema: './src/db/schema.ts',
  dialect: 'mysql',
  dbCredentials: {
    url: process.env.DATABASE_URL!,
  },
});

Common Query Patterns

Select Queries

// All rows
const users = await db.select().from(users);

// With WHERE
const user = await db.select()
  .from(users)
  .where(eq(users.id, 1));

// Multiple conditions (AND)
import { and } from "drizzle-orm";
const activeUsers = await db.select()
  .from(users)
  .where(and(
    eq(users.active, true),
    gt(users.createdAt, new Date('2024-01-01'))
  ));

// Multiple conditions (OR)
import { or } from "drizzle-orm";
const result = await db.select()
  .from(users)
  .where(or(
    eq(users.role, 'admin'),
    eq(users.role, 'moderator')
  ));

// Limit & Offset
const recentUsers = await db.select()
  .from(users)
  .orderBy(users.createdAt)
  .limit(10)
  .offset(20);

Insert Queries

// Single insert
await db.insert(users).values({
  name: "John",
  email: "john@example.com"
});

// Multiple inserts
await db.insert(users).values([
  { name: "Alice", email: "alice@example.com" },
  { name: "Bob", email: "bob@example.com" }
]);

// Insert with RETURNING (PostgreSQL only)
const [newUser] = await db.insert(users).values({
  name: "John",
  email: "john@example.com"
}).returning();

Update Queries

// Update
await db.update(users)
  .set({ name: "Jane Doe" })
  .where(eq(users.id, 1));

// Update with RETURNING (PostgreSQL)
const [updatedUser] = await db.update(users)
  .set({ name: "Jane Doe" })
  .where(eq(users.id, 1))
  .returning();

Delete Queries

// Delete
await db.delete(users)
  .where(eq(users.id, 1));

// Delete with RETURNING (PostgreSQL)
const [deletedUser] = await db.delete(users)
  .where(eq(users.id, 1))
  .returning();

Joins

import { eq } from "drizzle-orm";

// Left join
const usersWithPosts = await db.select()
  .from(users)
  .leftJoin(posts, eq(users.id, posts.authorId));

// Inner join
const usersWithPublishedPosts = await db.select()
  .from(users)
  .innerJoin(posts, eq(users.id, posts.authorId))
  .where(eq(posts.published, true));

// Select specific columns
const result = await db.select({
  userName: users.name,
  postTitle: posts.title
})
  .from(users)
  .leftJoin(posts, eq(users.id, posts.authorId));

Aggregations

import { count, sum, avg } from "drizzle-orm";

// Count
const [{ total }] = await db.select({
  total: count()
}).from(users);

// Count with GROUP BY
const postCounts = await db.select({
  authorId: posts.authorId,
  count: count()
})
  .from(posts)
  .groupBy(posts.authorId);

Relations

Define Relations

import { relations } from "drizzle-orm";

export const usersRelations = relations(users, ({ many }) => ({
  posts: many(posts),
}));

export const postsRelations = relations(posts, ({ one }) => ({
  author: one(users, {
    fields: [posts.authorId],
    references: [users.id],
  }),
}));

Query Relations

// Include related data
const usersWithPosts = await db.query.users.findMany({
  with: {
    posts: true
  }
});

// Nested relations
const usersWithPublishedPosts = await db.query.users.findMany({
  with: {
    posts: {
      where: eq(posts.published, true)
    }
  }
});

TypeScript Types

Infer Types from Schema

import { users, posts } from "./db/schema";
import type { InferSelectModel, InferInsertModel } from "drizzle-orm";

// Select types (what you get from SELECT queries)
type User = InferSelectModel<typeof users>;
type Post = InferSelectModel<typeof posts>;

// Insert types (what you need for INSERT queries)
type NewUser = InferInsertModel<typeof users>;
type NewPost = InferInsertModel<typeof posts>;

// Usage
const user: User = await db.select()
  .from(users)
  .where(eq(users.id, 1))
  .then(rows => rows[0]);

const newUser: NewUser = {
  name: "John",
  email: "john@example.com"
};
await db.insert(users).values(newUser);

Best Practices

  1. Use prepared statements (postgres.js: prepare: true)
  2. Set max: 5 for connection pools
  3. Use ctx.waitUntil() for cleanup
  4. Define types from schema with InferSelectModel
  5. Use relations for complex queries instead of manual joins
  6. Run migrations outside of Worker (use drizzle-kit CLI)
  7. Use .env for migrations (DATABASE_URL), not in Worker
  8. Version control migrations in drizzle/ folder

Drizzle vs Raw SQL

Drizzle ORM

Pros:

  • Type-safe queries
  • Auto-completion in IDE
  • Compile-time error checking
  • Easy migrations with drizzle-kit
  • Relational queries with db.query

Cons:

  • Slight learning curve
  • More setup than raw SQL

Raw SQL

Pros:

  • Full SQL control
  • Simpler for simple queries
  • No ORM overhead

Cons:

  • No type safety
  • Manual type definitions
  • More error-prone

Recommendation: Use Drizzle for type safety and developer experience.


References