Prompt directory
Impulse.directory
HomeBrowseStacks
Sign inCreate an account
    rule

    Drizzle ORM best practices

    impulse-lab/drizzle-orm-best-practices·v1·updated September 25, 2025
    Leonard Roussard
    Leonard Roussard@lionvsx·Impulse Lab
    Cursor / Rules
    Content
    ---
    description: Best practices for using Drizzle ORM with PostgreSQL
    globs:
    alwaysApply: false
    ---
    
    # Drizzle ORM Best Practices
    Standards for using Drizzle ORM with PostgreSQL. Auto-included for TypeScript and TSX files in db directories.
    <rule>
    name: drizzle_orm_best_practices
    description: Best practices for using Drizzle ORM with PostgreSQL. Auto-included for TypeScript and TSX files in db directories.
    globs: ["**/db/*.{ts,tsx}", "**/server/**/*.{ts,tsx}"]
    filters:
    - type: file_extension
    pattern: "\.(ts|tsx)$"
    - type: path
    pattern: ".*/(?:db|server)/.*"
    actions:
    - type: suggest
    message: |
    Follow these Drizzle ORM best practices:
        1. Schema Definition:
            - Use proper column types
            - Export types for each schema
            - Define explicit relationships
            - Use proper naming conventions
            - Use ULID for ID fields with $defaultFn:
            - `// Bad: Using serial or UUID without $defaultFn
    id: serial('id').primaryKey(),
    // or
    id: uuid('id').primaryKey().defaultRandom(),
    
    // Good: Using ULID with $defaultFn
    import { ulid } from "ulid";
    
    id: text("id")
       .primaryKey()
       .$defaultFn(() => ulid()),
    `
            - Use `.$type<T>()` for JSON field type definitions and for any column that stores a value with a TypeScript type (enums, unions, objets, etc.):
            - `// Bad: Column without type information
    vote: varchar("vote", { length: 8 }).notNull(),
    
    // Good: Column with explicit type
    vote: varchar("vote", { length: 8 }).notNull().$type<MessageFeedbackVote>(),
    
    // Good: JSON field with type definition
    settings: json('settings').$type<UserSettings>()
    `
            - Avoid using deprecated third argument in pgTable:
            - `// Bad: Using deprecated third argument
    export const users = pgTable('users', {
      // columns
    }, (table) => {
      return {
        uniqueIdx: uniqueIndex('unique_idx').on(table.email)
      };
    });
    
    // Good: Using the new API without third argument
    export const users = pgTable('users', {
      // columns
    });
    
    // Create indexes separately
    export const usersEmailIndex = uniqueIndex('users_email_idx').on(users.email);
    `
        2. Type Exports:
            - Export table types using typeof
            - Export inferred types for each table
            - Export relationship types
            - Use proper type naming conventions
        3. Relationships:
            - Define explicit relationships using references:
            - `// Good: One-to-many relationship
    export const users = pgTable('users', {
      id: serial('id').primaryKey(),
      teamId: integer('team_id').references(() => teams.id, { onDelete: 'cascade' })
    });
    
    // Good: Many-to-many relationship
    import { primaryKey } from 'drizzle-orm/pg-core';
    export const userToProjects = pgTable('user_to_projects', {
      userId: integer('user_id').references(() => users.id),
      projectId: integer('project_id').references(() => projects.id),
    }, (table) => [
      primaryKey({ columns: [table.userId, table.projectId] })
    ]);
    
    // Good: One-to-one relationship
    export const users = pgTable('users', {
      id: serial('id').primaryKey(),
      // Other columns
    });
    
    export const profiles = pgTable('profiles', {
      id: serial('id').primaryKey(),
      userId: integer('user_id').notNull().unique().references(() => users.id, { onDelete: 'cascade' })
    });
    `
            - Implement relations setup in queries:
            - `import { relations } from 'drizzle-orm';
    
    export const usersRelations = relations(users, ({ one, many }) => ({
      profile: one(profiles, {
        fields: [users.id],
        references: [profiles.userId],
      }),
      posts: many(posts),
      team: one(teams, {
        fields: [users.teamId],
        references: [teams.id],
      }),
    }));
    
    export const teamsRelations = relations(teams, ({ many }) => ({
      users: many(users),
    }));
    `
        4. Query Building:
            - Use type-safe queries
            - Implement proper joins
            - Use prepared statements
            - Handle complex queries properly
        5. Performance & Security:
            - Use proper indexes
            - Implement query optimization
            - Use parameterized queries
            - Handle sensitive data properly
        6. Validators:
            - Place validators unrelated to database schemas in `/validators` folder
            - Use Drizzle Zod for schema validation:
            - `import { createInsertSchema, createSelectSchema } from 'drizzle-zod';
    
    // Generate Zod schema from Drizzle schema
    export const insertUserSchema = createInsertSchema(users);
    export const selectUserSchema = createSelectSchema(users);
    
    // Extend generated schema with additional validations
    export const extendedUserSchema = insertUserSchema.extend({
      email: z.string().email("Invalid email format"),
      password: z.string().min(8, "Password must be at least 8 characters"),
    });
    
    // For validators unrelated to DB schemas, create them in /validators folder
    // /validators/contactForm.ts
    export const contactFormSchema = z.object({
      name: z.string().min(2, "Name must be at least 2 characters"),
      email: z.string().email("Invalid email format"),
      message: z.string().min(10, "Message must be at least 10 characters"),
    });
    `
        7. Indexing Best Practices:
            - When to add indexes:
    
                - Add indexes on columns used frequently in WHERE clauses
                - Add indexes on columns used in ORDER BY or GROUP BY
                - Add indexes on foreign key columns
                - Avoid over-indexing as it slows down writes and increases storage
            - Types of indexes to use:
            - `// Primary key index (automatically created)
    id: serial('id').primaryKey(),
    
    // Unique index for emails
    export const usersEmailIndex = uniqueIndex('users_email_idx').on(users.email);
    
    // Multi-column index for composite lookups
    export const orderDateCustomerIndex = index('order_date_customer_idx')
      .on(orders.orderDate, orders.customerId);
    
    // Partial index (with condition)
    export const activeUsersIndex = index('active_users_idx', {
      where: eq(users.isActive, true)
    }).on(users.lastLogin);
    
    // Full-text search index (for text columns)
    export const productsSearchIndex = index('products_search_idx')
      .using('gin')
      .on(sql`to_tsvector('english', ${products.description})`);
    `
            - Naming conventions for indexes:
    
                - Use format: table_column(s)_idx
                - For unique indexes: table_column(s)_unique_idx
                - For multi-column: table_column1_column2_idx
        8. File Organization (Modular Table Pattern):
    **PREFERRED: One folder per table structure**
        9. `/db
    ├── index.ts           # Export db client
    ├── schema
    │   ├── index.ts       # Export all schema modules
    │   ├── auth/          # Authentication tables (can contain multiple related tables)
    │   │   ├── schema.ts  # Table definitions
    │   │   ├── relation.ts # Relationships
    │   │   ├── type.ts    # Types and validators
    │   │   └── index.ts   # Module exports
    │   ├── user-preferences/  # Single table module
    │   │   ├── schema.ts
    │   │   ├── relation.ts
    │   │   ├── type.ts
    │   │   └── index.ts
    │   └── notification-settings/  # Single table module
    │       ├── schema.ts
    │       ├── relation.ts
    │       ├── type.ts
    │       └── index.ts
    └── migrations/        # Migration files
    `
        10. **Modular Structure Guidelines:**
            - Each table gets its own folder OR related tables share a folder (like auth)
            - Each folder contains 4 files: schema.ts, relation.ts, type.ts, index.ts
            - NO COMMENTS in schema files - code should be self-explanatory
            - Use descriptive variable and function names instead of comments
        11. **File Templates:**
            1. **schema.ts** - Table definitions only:
        12. `import { pgTable, text, timestamp, boolean } from "drizzle-orm/pg-core";
    import { ulid } from "ulid";
    
    export const userPreferences = pgTable("user_preferences", {
        id: text("id")
            .primaryKey()
            .$defaultFn(() => ulid()),
        userId: text("user_id")
            .notNull()
            .references(() => user.id, { onDelete: "cascade" })
            .unique(),
        language: text("language").notNull().default("en-US"),
        timezone: text("timezone").notNull().default("GMT+01:00"),
        createdAt: timestamp("created_at").notNull().defaultNow(),
        updatedAt: timestamp("updated_at").notNull().defaultNow(),
    });
    `
            2. **relation.ts** - Relationships only:
        13. `import { relations } from "drizzle-orm";
    import { user } from "../auth/schema";
    import { userPreferences } from "./schema";
    
    export const userPreferencesRelations = relations(
        userPreferences,
        ({ one }) => ({
            user: one(user, {
                fields: [userPreferences.userId],
                references: [user.id],
            }),
        })
    );
    `
            3. **type.ts** - Types and validators only:
        14. `import { createInsertSchema } from "drizzle-zod";
    import { userPreferences } from "./schema";
    
    export type UserPreferences = typeof userPreferences.$inferSelect;
    export type NewUserPreferences = typeof userPreferences.$inferInsert;
    
    export const userPreferencesSchema = createInsertSchema(userPreferences).omit({
        userId: true,
        id: true,
    });
    `
            4. **index.ts** - Module exports:
        15. `export * from "./schema";
    export * from "./relation";
    export * from "./type";
    `
        16. **Cross-Module References:**
            - Import tables from other modules using relative paths:
            - `import { user } from "../auth/schema";
    `
            - Main schema index exports everything:
            - `// db/schema/index.ts
    export * from "./auth";
    export * from "./user-preferences";
    export * from "./notification-settings";
    `
        17. **Legacy Pattern (avoid for new projects):**
            - Single files per table group (users.ts, posts.ts)
            - Mixed concerns in single files
        18. Integration with tRPC:
            - Creating bridges between Drizzle and tRPC:
            - `// server/routers/users/queries/getUser.ts
    import { db } from '@/server/db';
    import { users } from '@/server/db/schema';
    import { eq } from 'drizzle-orm';
    
    export async function getUser({
      userId,
      db
    }: {
      userId: string,
      db: typeof db
    }) {
      // Use Drizzle queries inside tRPC procedure implementations
      return await db.query.users.findFirst({
        where: eq(users.id, userId),
        with: {
          profile: true,
          posts: true
        }
      });
    }
    
    // server/routers/users/router.ts
    import { router, protectedProcedure } from '@/server/trpc';
    import { getUser } from './queries/getUser';
    import { getUserInputSchema } from './validators';
    
    export const usersRouter = router({
      getUser: protectedProcedure
        .input(getUserInputSchema)
        .query(async ({ ctx, input }) => {
          const { db } = ctx;
          // Pass db from context to the implementation function
          return await getUser({ userId: input.userId, db });
        })
    });
    `
            - Consistent error handling:
            - `export async function createUser({
      input,
      db
    }: {
      input: NewUser,
      db: typeof db
    }) {
      try {
        // Attempt to create user with Drizzle
        const [user] = await db.insert(users)
          .values(input)
          .returning();
        return { success: true, user };
      } catch (error) {
        // Handle specific database errors
        if (error.code === '23505') { // Unique violation
          throw new TRPCError({
            code: 'CONFLICT',
            message: 'User with this email already exists'
          });
        }
        // Re-throw other errors for tRPC to handle
        throw new TRPCError({
          code: 'INTERNAL_SERVER_ERROR',
          message: 'Failed to create user'
        });
      }
    }
    `
            - Type safety between Drizzle and tRPC:
            - `// Leverage Drizzle's type system with tRPC
    import { users } from '@/server/db/schema';
    import { z } from 'zod';
    import { createInsertSchema } from 'drizzle-zod';
    
    // Create Zod schema from Drizzle schema for tRPC input validation
    export const userInputSchema = createInsertSchema(users).omit({
      id: true, // Remove auto-generated fields
      createdAt: true
    });
    
    // Use in tRPC procedure
    export const createUser = protectedProcedure
      .input(userInputSchema)
      .mutation(async ({ ctx, input }) => {
        // Input is fully typed from Drizzle schema
        return await createUserImpl({ input, db: ctx.db });
      });
    `
    examples:
    - input: |
    // Bad: Schema without type exports and relationships
    export const users = pgTable('users', {
    id: serial('id').primaryKey(),
    name: text('name'),
    email: varchar('email', { length: 255 })
    });
    - // Good: Schema with type exports and relationships
    import { InferModel } from 'drizzle-orm';
    import { pgTable, serial, text, varchar, foreignKey } from 'drizzle-orm/pg-core';
    - export const users = pgTable('users', {
    id: serial('id').primaryKey(),
    name: text('name').notNull(),
    email: varchar('email', { length: 255 }).notNull().unique(),
    teamId: integer('team_id').references(() => teams.id, { onDelete: 'cascade' })
    });
    - export const teams = pgTable('teams', {
    id: serial('id').primaryKey(),
    name: text('name').notNull()
    });
    - // Type exports
    export type User = InferModel<typeof users>;
    export type NewUser = InferModel<typeof users, 'insert'>;
    export type Team = InferModel<typeof teams>;
    export type NewTeam = InferModel<typeof teams, 'insert'>;
    output: "Define schemas with proper type exports and relationships"
    - input: |
    // Bad: Not using relationships in queries
    const user = await db.select().from(users).where(eq(users.id, userId));
    - // Good: Using relationships in queries
    const userWithTeam = await db.select({
    id: users.id,
    name: users.name,
    team: {
    id: teams.id,
    name: teams.name
    }
    })
    .from(users)
    .leftJoin(teams, eq(users.teamId, teams.id))
    .where(eq(users.id, userId));
    output: "Use proper relationship queries with type safety"
    - input: |
    // Bad: JSON field without type information
    export const users = pgTable('users', {
    id: serial('id').primaryKey(),
    settings: json('settings')
    });
    - // Good: JSON field with type definition
    interface UserSettings {
    theme: 'light' | 'dark';
    notifications: boolean;
    language: string;
    }
    - export const users = pgTable('users', {
    id: serial('id').primaryKey(),
    settings: json('settings').$type<UserSettings>()
    });
    output: "Define type information for JSON fields using .$type<T>()"
    - input: |
    // Bad: Not using drizzle-zod for validation
    export const createUserSchema = z.object({
    name: z.string().min(2),
    email: z.string().email(),
    age: z.number().min(18)
    });
    - // Good: Using drizzle-zod for validation
    import { createInsertSchema } from 'drizzle-zod';
    - export const users = pgTable('users', {
    id: serial('id').primaryKey(),
    name: text('name').notNull(),
    email: varchar('email', { length: 255 }).notNull().unique(),
    age: integer('age').notNull()
    });
    - // Generate base schema
    export const insertUserSchema = createInsertSchema(users);
    - // Extend with additional validations
    export const createUserSchema = insertUserSchema.extend({
    name: z.string().min(2, "Name must be at least 2 characters"),
    email: z.string().email("Invalid email format"),
    age: z.number().min(18, "Must be at least 18 years old")
    });
    output: "Use drizzle-zod to generate and extend validation schemas from database tables"
    - input: |
    // Bad: Not using indexes for frequently queried columns
    export const users = pgTable('users', {
    id: serial('id').primaryKey(),
    email: varchar('email', { length: 255 }).notNull().unique(),
    status: varchar('status', { length: 50 }).notNull()
    });
    - // Good: Adding proper indexes for query optimization
    export const users = pgTable('users', {
    id: serial('id').primaryKey(),
    email: varchar('email', { length: 255 }).notNull().unique(),
    status: varchar('status', { length: 50 }).notNull()
    });
    - // Create a regular index for frequent status filtering
    export const usersStatusIndex = index('users_status_idx').on(users.status);
    - // Create a composite index for queries that filter by status and sort by created_at
    export const usersStatusCreatedAtIndex = index('users_status_created_at_idx')
    .on(users.status, users.createdAt);
    output: "Add appropriate indexes for frequent query patterns"
    - input: |
    // Bad: Disorganized schema files and exports
    // db/users.ts
    export const users = pgTable('users', {
    // columns
    });
    // db/profiles.ts
    export const profiles = pgTable('profiles', {
    // columns
    });
    - `                  // Good: Organized schema files with clear structure
                  // server/db/schema/index.ts
                  export * from './users';
                  export * from './posts';
    `
    - // server/db/schema/users.ts
    import { pgTable, serial, text, varchar } from 'drizzle-orm/pg-core';
    import { relations, index } from 'drizzle-orm';
    - // 1. Table definitions
    export const users = pgTable('users', {
    id: serial('id').primaryKey(),
    email: varchar('email', { length: 255 }).notNull(),
    name: text('name').notNull()
    });
    - // 2. Relationships
    export const usersRelations = relations(users, ({ many }) => ({
    posts: many(posts)
    }));
    - // 3. Indexes
    export const usersEmailIndex = index('users_email_idx').on(users.email);
    - // 4. Type exports
    export type User = typeof users.$inferSelect;
    export type NewUser = typeof users.$inferInsert;
    output: "Organize schema files with clear structure and exports"
    - `          - input: |
              // Bad: Mixed concerns in single file
              // db/schema/users.ts
              export const users = pgTable('users', { /* columns */ });
              export const userRelations = relations(users, ({ many }) => ({ /* relations */ }));
              export type User = typeof users.$inferSelect;
              export const userSchema = createInsertSchema(users);
    
              // Good: Modular structure with separated concerns
              // db/schema/users/schema.ts
              export const users = pgTable('users', { /* columns */ });
    
              // db/schema/users/relation.ts
              export const userRelations = relations(users, ({ many }) => ({ /* relations */ }));
    
              // db/schema/users/type.ts
              export type User = typeof users.$inferSelect;
              export const userSchema = createInsertSchema(users);
    
              // db/schema/users/index.ts
              export * from "./schema";
              export * from "./relation";
              export * from "./type";
    `
    - output: "Use modular folder structure for better organization and separation of concerns"
        - input: |
    // Bad: Disconnected Drizzle and tRPC logic
    // tRPC procedure with direct database access
    const getUserPosts = protectedProcedure
    .input(z.object({ userId: z.string() }))
    .query(async ({ ctx, input }) => {
    const posts = await ctx.db.select()
    .from(posts)
    .where(eq(posts.userId, input.userId));
    return posts;
    });
        - // Good: Bridging Drizzle with tRPC using clean implementation functions
    // server/routers/posts/queries/getUserPosts.ts
    export async function getUserPosts({
    userId,
    db
    }: {
    userId: string,
    db: DB
    }) {
    return await db.query.posts.findMany({
    where: eq(posts.userId, userId),
    orderBy: [desc(posts.createdAt)],
    with: {
    author: {
    columns: {
    id: true,
    name: true
    }
    }
    }
    });
    }
        - // server/routers/posts/router.ts
    export const postsRouter = router({
    getUserPosts: protectedProcedure
    .input(z.object({ userId: z.string() }))
    .query(async ({ ctx, input }) => {
    return await getUserPosts({ userId: input.userId, db: ctx.db });
    })
    });
    output: "Create a clean bridge between Drizzle and tRPC with implementation functions"
    metadata:
    priority: high
    version: 1.3
    </rule>

    Install with the impulse CLI

    $npx @impulselab/directory impulselab/drizzle-orm-best-practices

    Stats

    Views
    3
    Saves
    0

    Owner

    impulse-logo

    Impulse Lab

    Official artifact