Skills
drizzle-patterns
drizzle-patterns
npx @loomcraft/cli add skill drizzle-patternsFrontmatter
Name
drizzle-patterns
Description
Drizzle ORM patterns for schema design, migrations, transactions, and DAO functions. Use when creating database schemas, writing queries, implementing transactions, or working with PostgreSQL via Drizzle.
Content
# Drizzle ORM Patterns
## Critical Rules
- **Infer types from schema** — never duplicate type definitions manually.
- **Use `limit()` on all queries** — never fetch unbounded result sets.
- **Select only needed columns** — avoid `select()` with no arguments on large tables.
- **Never nest transactions** — keep transactions short with no external API calls inside.
- **Never edit generated migration files** — review SQL before applying in production.
- **Add indexes** on columns used in `WHERE`, `JOIN`, and `ORDER BY`.
## Schema Design
- Define schemas in `src/schema/` with one file per entity.
- Export all tables from `src/schema/index.ts` barrel file.
- Use `pgTable` for table definitions with typed columns.
```ts
// src/schema/user.ts
import { pgTable, uuid, text, timestamp, boolean } from "drizzle-orm/pg-core";
export const users = pgTable("users", {
id: uuid("id").primaryKey().defaultRandom(),
email: text("email").notNull().unique(),
name: text("name").notNull(),
role: text("role", { enum: ["USER", "ADMIN"] }).notNull().default("USER"),
emailVerified: boolean("email_verified").notNull().default(false),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
});
```
## Relations
- Define relations separately using `relations()`:
```ts
import { relations } from "drizzle-orm";
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
organizations: many(organizationMembers),
}));
export const postsRelations = relations(posts, ({ one }) => ({
author: one(users, { fields: [posts.authorId], references: [users.id] }),
}));
```
## Database Client
```ts
// src/lib/db.ts
import { drizzle } from "drizzle-orm/node-postgres";
import * as schema from "@/schema";
export const db = drizzle(process.env.DATABASE_URL!, { schema });
```
## Query Patterns
- Use the query builder for complex reads:
```ts
const result = await db.query.users.findMany({
where: eq(users.role, "ADMIN"),
with: { posts: true },
orderBy: [desc(users.createdAt)],
limit: 20,
});
```
- Use `select()` for simple reads with specific columns:
```ts
const result = await db.select({
id: users.id,
name: users.name,
}).from(users).where(eq(users.role, "ADMIN"));
```
## Transactions
- Wrap multi-table writes in transactions:
```ts
await db.transaction(async (tx) => {
const [org] = await tx.insert(organizations).values({ name }).returning();
await tx.insert(organizationMembers).values({
organizationId: org.id,
userId: currentUser.id,
role: "OWNER",
});
});
```
## DAO Pattern
- Create DAO functions in `src/dal/` for reusable queries:
```ts
// src/dal/user.dal.ts
export async function findUserById(id: string) {
return db.query.users.findFirst({
where: eq(users.id, id),
});
}
export async function findUsersByOrg(orgId: string) {
return db.select()
.from(users)
.innerJoin(orgMembers, eq(orgMembers.userId, users.id))
.where(eq(orgMembers.orgId, orgId));
}
export async function createUser(data: NewUser) {
const [user] = await db.insert(users).values(data).returning();
return user;
}
export async function updateUser(id: string, data: Partial<NewUser>) {
const [user] = await db.update(users)
.set({ ...data, updatedAt: new Date() })
.where(eq(users.id, id))
.returning();
return user;
}
export async function deleteUser(id: string) {
await db.delete(users).where(eq(users.id, id));
}
```
## Migrations
- Generate migrations with `npx drizzle-kit generate`.
- Apply with `npx drizzle-kit migrate`.
- Use `drizzle-kit push` only in development for quick iteration.
- Review generated SQL before applying in production.
## Type Inference
```ts
import type { InferSelectModel, InferInsertModel } from "drizzle-orm";
export type User = InferSelectModel<typeof users>;
export type NewUser = InferInsertModel<typeof users>;
```
## Performance
- Add indexes on columns used in `WHERE`, `JOIN`, and `ORDER BY`:
```ts
import { index } from "drizzle-orm/pg-core";
export const posts = pgTable("posts", {
// columns...
}, (table) => [
index("posts_author_idx").on(table.authorId),
index("posts_created_at_idx").on(table.createdAt),
]);
```
- Use `prepare()` for frequently executed queries.
## Do
- Infer TypeScript types from the schema with `InferSelectModel` / `InferInsertModel` -- never hand-write duplicates.
- Always pass `limit()` on every query; paginate large collections.
- Select only the columns you need -- use explicit column lists in `select({})`.
- Keep transactions short -- insert/update and return, nothing else.
- Review generated migration SQL before applying it to staging or production.
- Add composite indexes when queries filter or sort on multiple columns together.
- Use `.returning()` on insert/update to get the created/updated row back in one round-trip.
- Co-locate relation definitions next to the table they belong to.
## Don't
- Don't call `select()` with no arguments on tables with many columns -- it fetches everything.
- Don't nest `db.transaction()` calls -- Drizzle does not support savepoints this way.
- Don't make HTTP or external API calls inside a transaction block.
- Don't manually edit files inside the `drizzle/` migrations folder -- regenerate instead.
- Don't use `drizzle-kit push` in production -- it skips migration history.
- Don't scatter raw SQL strings across services -- wrap them in DAL functions.
- Don't forget `.notNull()` on columns that should never be null -- the default is nullable.
- Don't store enum values as free-text columns when a Drizzle `text({ enum })` or `pgEnum` fits.
## Anti-Patterns
| Anti-Pattern | Problem | Fix |
|---|---|---|
| **Unbounded select** | `db.select().from(users)` with no `limit()` returns the entire table, causing memory spikes. | Always chain `.limit(n)` and implement cursor or offset pagination. |
| **Fat transactions** | Long-running transactions that call external APIs hold database locks and risk timeouts. | Move external calls outside the transaction; keep only DB operations inside. |
| **Manual type duplication** | Writing `type User = { id: string; ... }` by hand instead of inferring from the schema. | Use `InferSelectModel<typeof users>` and `InferInsertModel<typeof users>`. |
| **Editing migration files** | Hand-editing generated SQL causes drift between schema and migration history. | Regenerate migrations with `drizzle-kit generate` after changing the schema. |
| **Missing indexes** | Queries on non-indexed columns cause full table scans as data grows. | Add `index()` on every column used in `WHERE`, `JOIN`, or `ORDER BY`. |
| **God DAL file** | One giant `db.dal.ts` mixing queries for every entity. | One file per entity: `user.dal.ts`, `post.dal.ts`, etc. |Files
No additional files