Skill

Master Drizzle ORM for Type-Safe Databases

Drizzle ORM Expert covers type-safe schema design, relational queries, Drizzle Kit migrations, and Next.js/serverless database integration.

Works with next.jstrpchononeonplanetscale

Maintainer of this project? Claim this page to edit the listing.


81
Spark score
out of 100
Updated last month
Version 1.0.0

Add to Favorites

Why it matters

Build robust, type-safe, and performant database layers using Drizzle ORM with TypeScript. This asset guides you through schema design, complex queries, migrations, and integrations with modern frameworks and serverless databases.

Outcomes

What it gets done

01

Set up Drizzle ORM in new or existing projects.

02

Design database schemas with Drizzle's TypeScript-first approach.

03

Write complex relational queries and optimize database performance.

04

Manage Drizzle Kit migrations and integrate with Next.js, tRPC, or Hono.

Install

Add it to your toolbox

Run in your project directory:

curl -fsSL https://spark.entire.vc/get/ag-drizzle-orm-expert | bash

Overview

Drizzle ORM Expert

Provides production-grade Drizzle ORM guidance covering TypeScript-first schema design, relational and SQL-like queries, Drizzle Kit migrations, serverless database client setup, and Next.js integration. Use for setting up, designing, querying, migrating, or optimizing a Drizzle ORM database layer, or migrating to Drizzle from Prisma, TypeORM, or Knex.

What it does

Drizzle ORM Expert provides production-grade guidance for building type-safe, performant database layers with Drizzle ORM and TypeScript. Drizzle compiles to raw SQL with zero runtime overhead - unlike Prisma's query-engine-binary approach - making it well suited to edge and serverless runtimes (Cloudflare Workers, Vercel Edge, Deno). Its core advantages are a SQL-like API, zero dependencies, full compile-time type inference from schema to query, and a Prisma-like relational query API that avoids N+1 problems.

It covers schema design (pgTable definitions with enums, foreign keys with onDelete behavior, and relations() for one-to-many/many-to-one links), type inference via InferSelectModel/InferInsertModel instead of hand-written interfaces, and the full query surface: SQL-like select/where/innerJoin/groupBy/aggregation queries, the relational query API (db.query.users.findMany({ with: { posts: {...} } })) for nested data in a single query, insert/update/delete including batch inserts and .returning(), and transactions via db.transaction().

For migrations it documents the Drizzle Kit workflow - a drizzle.config.ts pointing at the schema and dialect, then drizzle-kit generate to create migration SQL from schema changes, drizzle-kit push for direct (development-only) schema sync, drizzle-kit migrate to apply migrations in production, and drizzle-kit studio as a GUI database browser. It shows database client setup for Postgres via Neon serverless, SQLite via Turso/LibSQL, and MySQL via PlanetScale, plus performance techniques - prepared statements (.prepare() / .execute()), db.batch() for multiple independent queries in one round trip, and schema-level indexing with index()/uniqueIndex(). It also covers Next.js integration patterns for React Server Components and Server Actions ("use server").

Best practices: keep schema in db/schema.ts or split by domain, use InferSelectModel/InferInsertModel for type safety, use the relational query API to avoid N+1s, use prepared statements for hot queries, and use generate + migrate in production - never push, which can cause data loss. It also warns against writing raw SQL when the query builder already supports the operation, forgetting relations() when using db.query.* with with, and creating a new database connection per request in serverless instead of pooling. Common troubleshooting: db.query.tableName being undefined is fixed by passing the full schema (including relations) to drizzle(); migration conflicts after schema changes are resolved by re-running drizzle-kit generate then migrate; and MySQL type errors on .returning() are explained by MySQL not supporting RETURNING - use .execute() and read insertId instead.

When to use - and when NOT to

Use this skill when setting up Drizzle ORM in a new or existing project, designing TypeScript-first database schemas, writing complex relational queries (joins, subqueries, aggregations), setting up or troubleshooting Drizzle Kit migrations, integrating Drizzle with Next.js App Router, tRPC, or Hono, optimizing database performance (prepared statements, batching, connection pooling), or migrating from Prisma, TypeORM, or Knex to Drizzle. Do not treat the output as a substitute for environment-specific validation, testing, or expert review, and stop to ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.

Inputs and outputs

Inputs are the target database dialect (Postgres, SQLite, or MySQL), the hosting/serverless provider (Neon, Turso, PlanetScale, or Supabase), and the desired schema or query. Output is working Drizzle schema, query, migration, or client-setup TypeScript code following the patterns above.

Integrations

Works with Next.js App Router (Server Components and Server Actions), tRPC, Hono, and serverless/edge database providers Neon, Turso/LibSQL, PlanetScale, and Supabase, via the corresponding drizzle-orm driver packages (drizzle-orm/neon-http, drizzle-orm/libsql, drizzle-orm/planetscale-serverless).

Who it's for

TypeScript backend and full-stack developers building or migrating a database layer on Drizzle ORM, especially in edge/serverless environments where Prisma's runtime engine is a poor fit.

Source README

Drizzle ORM Expert

You are a production-grade Drizzle ORM expert. You help developers build type-safe, performant database layers using Drizzle ORM with TypeScript. You know schema design, the relational query API, Drizzle Kit migrations, and integrations with Next.js, tRPC, and serverless databases (Neon, PlanetScale, Turso, Supabase).

When to Use This Skill

  • Use when the user asks to set up Drizzle ORM in a new or existing project
  • Use when designing database schemas with Drizzle's TypeScript-first approach
  • Use when writing complex relational queries (joins, subqueries, aggregations)
  • Use when setting up or troubleshooting Drizzle Kit migrations
  • Use when integrating Drizzle with Next.js App Router, tRPC, or Hono
  • Use when optimizing database performance (prepared statements, batching, connection pooling)
  • Use when migrating from Prisma, TypeORM, or Knex to Drizzle

Core Concepts

Why Drizzle

Drizzle ORM is a TypeScript-first ORM that generates zero runtime overhead. Unlike Prisma (which uses a query engine binary), Drizzle compiles to raw SQL - making it ideal for edge runtimes and serverless. Key advantages:

  • SQL-like API: If you know SQL, you know Drizzle
  • Zero dependencies: Tiny bundle, works in Cloudflare Workers, Vercel Edge, Deno
  • Full type inference: Schema → types → queries are all connected at compile time
  • Relational Query API: Prisma-like nested includes without N+1 problems

Schema Design Patterns

Table Definitions

// db/schema.ts
import { pgTable, text, integer, timestamp, boolean, uuid, pgEnum } from "drizzle-orm/pg-core";
import { relations } from "drizzle-orm";

// Enums
export const roleEnum = pgEnum("role", ["admin", "user", "moderator"]);

// Users table
export const users = pgTable("users", {
  id: uuid("id").defaultRandom().primaryKey(),
  email: text("email").notNull().unique(),
  name: text("name").notNull(),
  role: roleEnum("role").default("user").notNull(),
  createdAt: timestamp("created_at").defaultNow().notNull(),
  updatedAt: timestamp("updated_at").defaultNow().notNull(),
});

// Posts table with foreign key
export const posts = pgTable("posts", {
  id: uuid("id").defaultRandom().primaryKey(),
  title: text("title").notNull(),
  content: text("content"),
  published: boolean("published").default(false).notNull(),
  authorId: uuid("author_id").references(() => users.id, { onDelete: "cascade" }).notNull(),
  createdAt: timestamp("created_at").defaultNow().notNull(),
});

Relations

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

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

Type Inference

// Infer types directly from your schema - no separate type files needed
import type { InferSelectModel, InferInsertModel } from "drizzle-orm";

export type User = InferSelectModel<typeof users>;
export type NewUser = InferInsertModel<typeof users>;
export type Post = InferSelectModel<typeof posts>;
export type NewPost = InferInsertModel<typeof posts>;

Query Patterns

Select Queries (SQL-like API)

import { eq, and, like, desc, count, sql } from "drizzle-orm";

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

// Filtered with conditions
const admins = await db.select().from(users).where(eq(users.role, "admin"));

// Partial select (only specific columns)
const emails = await db.select({ email: users.email }).from(users);

// Join query
const postsWithAuthors = await db
  .select({
    title: posts.title,
    authorName: users.name,
  })
  .from(posts)
  .innerJoin(users, eq(posts.authorId, users.id))
  .where(eq(posts.published, true))
  .orderBy(desc(posts.createdAt))
  .limit(10);

// Aggregation
const postCounts = await db
  .select({
    authorId: posts.authorId,
    postCount: count(posts.id),
  })
  .from(posts)
  .groupBy(posts.authorId);

Relational Queries (Prisma-like API)

// Nested includes - Drizzle resolves in a single query
const usersWithPosts = await db.query.users.findMany({
  with: {
    posts: {
      where: eq(posts.published, true),
      orderBy: [desc(posts.createdAt)],
      limit: 5,
    },
  },
});

// Find one with nested data
const user = await db.query.users.findFirst({
  where: eq(users.id, userId),
  with: { posts: true },
});

Insert, Update, Delete

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

// Batch insert
await db.insert(posts).values([
  { title: "Post 1", authorId: newUser.id },
  { title: "Post 2", authorId: newUser.id },
]);

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

// Delete
await db.delete(posts).where(eq(posts.authorId, userId));

Transactions

const result = await db.transaction(async (tx) => {
  const [user] = await tx.insert(users).values({ email, name }).returning();
  await tx.insert(posts).values({ title: "Welcome Post", authorId: user.id });
  return user;
});

Migration Workflow (Drizzle Kit)

Configuration

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

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

Commands

# Generate migration SQL from schema changes
npx drizzle-kit generate

# Push schema directly to database (development only - skips migration files)
npx drizzle-kit push

# Run pending migrations (production)
npx drizzle-kit migrate

# Open Drizzle Studio (GUI database browser)
npx drizzle-kit studio

Database Client Setup

PostgreSQL (Neon Serverless)

// db/index.ts
import { drizzle } from "drizzle-orm/neon-http";
import { neon } from "@neondatabase/serverless";
import * as schema from "./schema";

const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle(sql, { schema });

SQLite (Turso/LibSQL)

import { drizzle } from "drizzle-orm/libsql";
import { createClient } from "@libsql/client";
import * as schema from "./schema";

const client = createClient({
  url: process.env.TURSO_DATABASE_URL!,
  authToken: process.env.TURSO_AUTH_TOKEN,
});
export const db = drizzle(client, { schema });

MySQL (PlanetScale)

import { drizzle } from "drizzle-orm/planetscale-serverless";
import { Client } from "@planetscale/database";
import * as schema from "./schema";

const client = new Client({ url: process.env.DATABASE_URL! });
export const db = drizzle(client, { schema });

Performance Optimization

Prepared Statements

// Prepare once, execute many times
const getUserById = db.query.users
  .findFirst({
    where: eq(users.id, sql.placeholder("id")),
  })
  .prepare("get_user_by_id");

// Execute with parameters
const user = await getUserById.execute({ id: "abc-123" });

Batch Operations

// Use db.batch() for multiple independent queries in one round-trip
const [allUsers, recentPosts] = await db.batch([
  db.select().from(users),
  db.select().from(posts).orderBy(desc(posts.createdAt)).limit(10),
]);

Indexing in Schema

import { index, uniqueIndex } from "drizzle-orm/pg-core";

export const posts = pgTable(
  "posts",
  {
    id: uuid("id").defaultRandom().primaryKey(),
    title: text("title").notNull(),
    authorId: uuid("author_id").references(() => users.id).notNull(),
    createdAt: timestamp("created_at").defaultNow().notNull(),
  },
  (table) => [
    index("posts_author_idx").on(table.authorId),
    index("posts_created_idx").on(table.createdAt),
  ]
);

Next.js Integration

Server Component Usage

// app/users/page.tsx (React Server Component)
import { db } from "@/db";
import { users } from "@/db/schema";

export default async function UsersPage() {
  const allUsers = await db.select().from(users);
  return (
    <ul>
      {allUsers.map((u) => (
        <li key={u.id}>{u.name}</li>
      ))}
    </ul>
  );
}

Server Action

// app/actions.ts
"use server";
import { db } from "@/db";
import { users } from "@/db/schema";

export async function createUser(formData: FormData) {
  const name = formData.get("name") as string;
  const email = formData.get("email") as string;
  await db.insert(users).values({ name, email });
}

Best Practices

  • Do: Keep all schema definitions in a single db/schema.ts or split by domain (db/schema/users.ts, db/schema/posts.ts)
  • Do: Use InferSelectModel and InferInsertModel for type safety instead of manual interfaces
  • Do: Use the relational query API (db.query.*) for nested data to avoid N+1 problems
  • Do: Use prepared statements for frequently executed queries in production
  • Do: Use drizzle-kit generate + migrate in production (never push)
  • Do: Pass { schema } to drizzle() to enable the relational query API
  • Don't: Use drizzle-kit push in production - it can cause data loss
  • Don't: Write raw SQL when the Drizzle query builder supports the operation
  • Don't: Forget to define relations() if you want to use db.query.* with with
  • Don't: Create a new database connection per request in serverless - use connection pooling

Troubleshooting

Problem: db.query.tableName is undefined
Solution: Pass all schema objects (including relations) to drizzle(): drizzle(client, { schema })

Problem: Migration conflicts after schema changes
Solution: Run npx drizzle-kit generate to create a new migration, then npx drizzle-kit migrate

Problem: Type errors on .returning() with MySQL
Solution: MySQL does not support RETURNING. Use .execute() and read insertId from the result instead.

Limitations

  • Use this skill only when the task clearly matches the scope described above.
  • Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
  • Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.

FAQ

Common questions

Discussion

Questions & comments · 0

Sign In Sign in to leave a comment.