drizzle

mgd34msu's avatarfrom mgd34msu

Builds type-safe database applications with Drizzle ORM including schema definition, queries, relations, and migrations. Use when working with databases in TypeScript, defining schemas, writing type-safe queries, or managing migrations.

⭐0stars🔀0forks📁View on GitHub🕐Updated Jan 4, 2026

When & Why to Use This Skill

This Claude skill enables developers to build robust, type-safe database applications using Drizzle ORM. It streamlines the entire database lifecycle—from defining schemas and managing migrations with Drizzle Kit to executing complex relational queries and transactions with full TypeScript support. By leveraging a SQL-first, lightweight approach, it ensures high performance and developer productivity across major databases including PostgreSQL, MySQL, and SQLite.

Use Cases

  • Schema Design & Type Inference: Rapidly defining database tables and relationships in TypeScript, allowing for automatic type generation for both select and insert operations.
  • Type-Safe Query Building: Writing complex SQL-like queries with full IDE autocompletion and compile-time checking to eliminate runtime database errors.
  • Automated Migration Management: Using Drizzle Kit to generate, push, and manage database schema versions, ensuring consistency across development and production environments.
  • Relational Data Fetching: Utilizing the 'Relational Queries' API to fetch deeply nested data structures (e.g., users with their posts and comments) in a single, optimized query without N+1 issues.
  • Multi-Database Support: Developing applications that require compatibility with different database engines like PostgreSQL, MySQL, or Edge-ready SQLite (Turso) using a unified syntax.
namedrizzle
descriptionBuilds type-safe database applications with Drizzle ORM including schema definition, queries, relations, and migrations. Use when working with databases in TypeScript, defining schemas, writing type-safe queries, or managing migrations.

Drizzle ORM

Lightweight, type-safe TypeScript ORM with zero dependencies and SQL-first approach.

Quick Start

Install:

# PostgreSQL
npm install drizzle-orm postgres
npm install -D drizzle-kit

# MySQL
npm install drizzle-orm mysql2
npm install -D drizzle-kit

# SQLite
npm install drizzle-orm better-sqlite3
npm install -D drizzle-kit @types/better-sqlite3

Project structure:

src/
  db/
    index.ts        # Database connection
    schema.ts       # Table definitions
    relations.ts    # Relation definitions
drizzle.config.ts   # Drizzle Kit config

Database Connection

PostgreSQL

// db/index.ts
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as schema from './schema';

const connectionString = process.env.DATABASE_URL!;
const client = postgres(connectionString);

export const db = drizzle(client, { schema });

MySQL

// db/index.ts
import { drizzle } from 'drizzle-orm/mysql2';
import mysql from 'mysql2/promise';
import * as schema from './schema';

const connection = await mysql.createConnection({
  host: 'localhost',
  user: 'root',
  database: 'mydb',
});

export const db = drizzle(connection, { schema });

SQLite

// db/index.ts
import { drizzle } from 'drizzle-orm/better-sqlite3';
import Database from 'better-sqlite3';
import * as schema from './schema';

const sqlite = new Database('sqlite.db');
export const db = drizzle(sqlite, { schema });

Turso (Edge SQLite)

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 });

Schema Definition

PostgreSQL Tables

// db/schema.ts
import {
  pgTable,
  serial,
  varchar,
  text,
  integer,
  boolean,
  timestamp,
  uuid,
  jsonb,
} from 'drizzle-orm/pg-core';

export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  email: varchar('email', { length: 255 }).notNull().unique(),
  name: varchar('name', { length: 100 }),
  bio: text('bio'),
  age: integer('age'),
  isActive: boolean('is_active').default(true),
  createdAt: timestamp('created_at').defaultNow(),
  updatedAt: timestamp('updated_at').defaultNow(),
});

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

export const comments = pgTable('comments', {
  id: serial('id').primaryKey(),
  content: text('content').notNull(),
  postId: uuid('post_id')
    .notNull()
    .references(() => posts.id, { onDelete: 'cascade' }),
  authorId: integer('author_id')
    .notNull()
    .references(() => users.id),
  createdAt: timestamp('created_at').defaultNow(),
});

MySQL Tables

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

export const users = mysqlTable('users', {
  id: serial('id').primaryKey(),
  email: varchar('email', { length: 255 }).notNull().unique(),
  name: varchar('name', { length: 100 }),
  createdAt: timestamp('created_at').defaultNow(),
});

SQLite Tables

import {
  sqliteTable,
  integer,
  text,
} from 'drizzle-orm/sqlite-core';

export const users = sqliteTable('users', {
  id: integer('id').primaryKey({ autoIncrement: true }),
  email: text('email').notNull().unique(),
  name: text('name'),
  createdAt: integer('created_at', { mode: 'timestamp' })
    .$defaultFn(() => new Date()),
});

Type Inference

// Infer types from schema
export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;

export type Post = typeof posts.$inferSelect;
export type NewPost = typeof posts.$inferInsert;

Relations

// db/relations.ts
import { relations } from 'drizzle-orm';
import { users, posts, comments } from './schema';

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

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

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

Many-to-Many

// Schema
export const usersToGroups = pgTable('users_to_groups', {
  userId: integer('user_id')
    .notNull()
    .references(() => users.id),
  groupId: integer('group_id')
    .notNull()
    .references(() => groups.id),
}, (t) => ({
  pk: primaryKey({ columns: [t.userId, t.groupId] }),
}));

// Relations
export const usersRelations = relations(users, ({ many }) => ({
  usersToGroups: many(usersToGroups),
}));

export const groupsRelations = relations(groups, ({ many }) => ({
  usersToGroups: many(usersToGroups),
}));

export const usersToGroupsRelations = relations(usersToGroups, ({ one }) => ({
  user: one(users, {
    fields: [usersToGroups.userId],
    references: [users.id],
  }),
  group: one(groups, {
    fields: [usersToGroups.groupId],
    references: [groups.id],
  }),
}));

Queries

Select

import { eq, lt, gte, ne, and, or, like, ilike, between, inArray } from 'drizzle-orm';
import { db } from './db';
import { users, posts } from './db/schema';

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

// Select specific columns
const userNames = await db.select({
  id: users.id,
  name: users.name,
}).from(users);

// Where clause
const activeUsers = await db.select()
  .from(users)
  .where(eq(users.isActive, true));

// Multiple conditions
const filteredUsers = await db.select()
  .from(users)
  .where(
    and(
      eq(users.isActive, true),
      gte(users.age, 18),
    )
  );

// OR conditions
const results = await db.select()
  .from(users)
  .where(
    or(
      eq(users.name, 'John'),
      eq(users.name, 'Jane'),
    )
  );

// Pattern matching
const matching = await db.select()
  .from(users)
  .where(like(users.email, '%@gmail.com'));

// Case-insensitive (PostgreSQL)
const matchingCI = await db.select()
  .from(users)
  .where(ilike(users.name, '%john%'));

// Range queries
const inRange = await db.select()
  .from(users)
  .where(between(users.age, 18, 65));

// IN operator
const specificUsers = await db.select()
  .from(users)
  .where(inArray(users.id, [1, 2, 3]));

Joins

import { eq } from 'drizzle-orm';

// Inner join
const postsWithAuthors = await db.select({
  post: posts,
  author: users,
})
  .from(posts)
  .innerJoin(users, eq(posts.authorId, users.id));

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

// Multiple joins
const fullData = await db.select()
  .from(posts)
  .innerJoin(users, eq(posts.authorId, users.id))
  .leftJoin(comments, eq(posts.id, comments.postId));

Ordering and Pagination

import { asc, desc } from 'drizzle-orm';

// Order by
const orderedUsers = await db.select()
  .from(users)
  .orderBy(desc(users.createdAt));

// Multiple order columns
const sorted = await db.select()
  .from(users)
  .orderBy(asc(users.name), desc(users.createdAt));

// Pagination
const page = await db.select()
  .from(users)
  .limit(10)
  .offset(20);

Aggregations

import { count, sum, avg, min, max } from 'drizzle-orm';

// Count
const userCount = await db.select({
  count: count(),
}).from(users);

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

// With having
const activeAuthors = await db.select({
  authorId: posts.authorId,
  postCount: count(posts.id),
})
  .from(posts)
  .groupBy(posts.authorId)
  .having(({ postCount }) => gte(postCount, 5));

Relational Queries

// Find many with relations
const usersWithPosts = await db.query.users.findMany({
  with: {
    posts: true,
  },
});

// Nested relations
const postsWithDetails = await db.query.posts.findMany({
  with: {
    author: true,
    comments: {
      with: {
        author: true,
      },
    },
  },
});

// Find first
const user = await db.query.users.findFirst({
  where: (users, { eq }) => eq(users.id, 1),
  with: {
    posts: true,
  },
});

// Select specific columns
const partialPosts = await db.query.posts.findMany({
  columns: {
    id: true,
    title: true,
  },
  with: {
    author: {
      columns: {
        name: true,
      },
    },
  },
});

// Filter nested relations
const recentPosts = await db.query.users.findMany({
  with: {
    posts: {
      where: (posts, { eq }) => eq(posts.published, true),
      orderBy: (posts, { desc }) => [desc(posts.createdAt)],
      limit: 5,
    },
  },
});

Insert

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

// Insert with returning (PostgreSQL, SQLite)
const [newUser] = await db.insert(users)
  .values({
    email: 'jane@example.com',
    name: 'Jane Doe',
  })
  .returning();

// Multiple insert
await db.insert(users).values([
  { email: 'user1@example.com', name: 'User 1' },
  { email: 'user2@example.com', name: 'User 2' },
]);

// Upsert (on conflict)
await db.insert(users)
  .values({
    email: 'john@example.com',
    name: 'John Updated',
  })
  .onConflictDoUpdate({
    target: users.email,
    set: { name: 'John Updated' },
  });

// On conflict do nothing
await db.insert(users)
  .values({ email: 'john@example.com', name: 'John' })
  .onConflictDoNothing({ target: users.email });

Update

// Update with where
await db.update(users)
  .set({ name: 'John Smith' })
  .where(eq(users.id, 1));

// Update with returning
const [updated] = await db.update(users)
  .set({ isActive: false })
  .where(eq(users.id, 1))
  .returning();

// Increment value
await db.update(posts)
  .set({ viewCount: sql`${posts.viewCount} + 1` })
  .where(eq(posts.id, 1));

Delete

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

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

// Delete all (be careful!)
await db.delete(users);

Transactions

// Basic transaction
await db.transaction(async (tx) => {
  const [user] = await tx.insert(users)
    .values({ email: 'new@example.com', name: 'New User' })
    .returning();

  await tx.insert(posts).values({
    title: 'First Post',
    authorId: user.id,
  });
});

// With rollback
await db.transaction(async (tx) => {
  await tx.insert(users).values({ email: 'test@example.com' });

  // Rollback on condition
  if (someCondition) {
    tx.rollback();
  }

  await tx.insert(posts).values({ title: 'Post', authorId: 1 });
});

// Nested transactions (savepoints)
await db.transaction(async (tx) => {
  await tx.insert(users).values({ email: 'outer@example.com' });

  await tx.transaction(async (tx2) => {
    await tx2.insert(posts).values({ title: 'Nested', authorId: 1 });
  });
});

Migrations

Configuration

// 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!,
  },
});

Commands

# Generate migration
npx drizzle-kit generate

# Apply migrations
npx drizzle-kit migrate

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

# Pull existing database schema
npx drizzle-kit pull

# Open Drizzle Studio
npx drizzle-kit studio

Raw SQL

import { sql } from 'drizzle-orm';

// Raw query
const result = await db.execute(sql`SELECT * FROM users WHERE id = ${userId}`);

// In select
const users = await db.select({
  id: users.id,
  fullName: sql<string>`${users.firstName} || ' ' || ${users.lastName}`,
}).from(users);

// Typed raw
const count = await db.execute<{ count: number }>(
  sql`SELECT COUNT(*) as count FROM users`
);

Prepared Statements

import { placeholder } from 'drizzle-orm';

// Prepare statement
const prepared = db.select()
  .from(users)
  .where(eq(users.id, placeholder('id')))
  .prepare('get_user');

// Execute with values
const user = await prepared.execute({ id: 1 });

// Relational prepared
const preparedWithPosts = db.query.users.findFirst({
  where: (users, { eq }) => eq(users.id, placeholder('id')),
  with: { posts: true },
}).prepare('user_with_posts');

const result = await preparedWithPosts.execute({ id: 1 });

Best Practices

  1. Export types from schema - Use $inferSelect and $inferInsert
  2. Use relations for nested data - Single query, no N+1
  3. Prepare frequent queries - Better performance
  4. Use transactions for multi-step ops - Ensure consistency
  5. Leverage push for development - Faster iteration

Common Mistakes

Mistake Fix
Missing schema in drizzle() Pass { schema } to enable query API
Not exporting relations Export from schema file
Using select() for relations Use db.query for relational queries
Raw SQL without typing Add type parameter to sql``
Forgetting returning() Add .returning() for inserted data

Reference Files