Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Expert in Drizzle ORM for TypeScript — schema design, relational queries, migrations, and serverless database integration. Use when building type-safe database layers with Drizzle.
.claude/skills/drizzle-orm-expert/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✗→✓ | ▲ Improved | — | — |
| case-08 | ✗→✓ | ▲ Improved | — | — |
| case-17 | ✗→✗ | = Same ✗ | — | — |
| case-22 | ✗→✗ | = Same ✗ | — | — |
| case-03 | ✗→✗ | = Same ✗ | — | — |
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).
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:
typescript// 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(), });
typescript// 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], }), }));
typescript// 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>;
typescriptimport { 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);
typescript// 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 }, });
typescript// 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));
typescriptconst 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; });
typescript// 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!, }, });
bash# 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
typescript// 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 });
typescriptimport { 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 });
typescriptimport { 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 });
typescript// 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" });
typescript// 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), ]);
typescriptimport { 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), ] );
typescript// 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> ); }
typescript// 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 }); }
db/schema.ts or split by domain (db/schema/users.ts, db/schema/posts.ts)InferSelectModel and InferInsertModel for type safety instead of manual interfacesdb.query.*) for nested data to avoid N+1 problemsdrizzle-kit generate + migrate in production (never push){ schema } to drizzle() to enable the relational query APIdrizzle-kit push in production — it can cause data lossrelations() if you want to use db.query.* with withProblem: 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.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-17 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-10 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-14 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-04 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +9 percentage points is the difference between those two pass rates over the 22 comparable cases.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
Other measured skills in the registry, with their headline benchmark lift.