https://orm.drizzle.team/docs/overview
https://orm.drizzle.team/docs/get-started
https://orm.drizzle.team/docs/sql-schema-declaration
https://orm.drizzle.team/docs/rqb
https://orm.drizzle.team/docs/select
https://orm.drizzle.team/docs/insert
https://orm.drizzle.team/docs/update
https://orm.drizzle.team/docs/delete
https://orm.drizzle.team/docs/transactions
https://orm.drizzle.team/docs/migrations
https://orm.drizzle.team/docs/drizzle-kit-generate
https://orm.drizzle.team/docs/drizzle-kit-migrate
https://orm.drizzle.team/docs/drizzle-kit-studio

1. Important Points#

Drizzle 是 TypeScript ORM:
    SQL-like API
    type-safe schema
    supports PostgreSQL / MySQL / SQLite
    migration with drizzle-kit
    good fit for TypeScript service

适合:
    team wants SQL control
    type-safe query is important
    schema lives in code
    migration should be explicit
    NestJS / Node.js API with relational database

不适合:
    team wants full Active Record style
    heavy dynamic query without type discipline
    no one owns SQL / schema design
核心原则:
    schema.ts is source of truth for application types
    migrations must be committed
    never run schema push directly in production
    keep database client as singleton provider
    use transaction for multi-write business operation
    validate external input before query
    use indexes based on real query patterns

2. Service Configuration#

install#

npm install drizzle-orm postgres
npm install -D drizzle-kit
postgres:
    lightweight PostgreSQL client

drizzle-orm:
    runtime query builder and typed ORM

drizzle-kit:
    generate / migrate / studio tooling

drizzle.config.ts#

import { defineConfig } from 'drizzle-kit';

export default defineConfig({
  schema: './src/database/schema.ts',
  out: './drizzle',
  dialect: 'postgresql',
  dbCredentials: {
    url: process.env.DATABASE_URL!,
  },
  strict: true,
  verbose: true,
});

package scripts#

{
  "scripts": {
    "db:generate": "drizzle-kit generate",
    "db:migrate": "drizzle-kit migrate",
    "db:studio": "drizzle-kit studio",
    "db:check": "drizzle-kit check"
  }
}
workflow:
    edit schema.ts
    npm run db:generate
    review generated SQL
    commit schema + migration files
    run npm run db:migrate in deployment

3. Schema / Data Modeling#

schema#

import {
  integer,
  pgEnum,
  pgTable,
  text,
  timestamp,
  uuid,
  varchar,
  index,
} from 'drizzle-orm/pg-core';

export const orderStatus = pgEnum('order_status', [
  'PENDING',
  'PAID',
  'CANCELLED',
]);

export const orders = pgTable(
  'orders',
  {
    id: uuid('id').primaryKey().defaultRandom(),
    userId: varchar('user_id', { length: 64 }).notNull(),
    status: orderStatus('status').notNull().default('PENDING'),
    amountCents: integer('amount_cents').notNull(),
    note: text('note'),
    createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
    updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
  },
  (table) => ({
    userCreatedIdx: index('orders_user_created_idx').on(table.userId, table.createdAt),
    statusCreatedIdx: index('orders_status_created_idx').on(table.status, table.createdAt),
  }),
);
schema rules:
    use explicit column name
    keep database naming snake_case
    keep TypeScript property naming camelCase
    add indexes for real query shape
    use enum only when values are stable
    use timestamp with timezone for cross-region systems

inferred types#

import { InferInsertModel, InferSelectModel } from 'drizzle-orm';
import { orders } from './schema';

export type Order = InferSelectModel<typeof orders>;
export type NewOrder = InferInsertModel<typeof orders>;
type usage:
    Order:
        row returned from database

    NewOrder:
        insert input shape

注意:
    DTO type != database insert type
    request validation still required

4. Query / Write Best Practices#

database client#

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

const client = postgres(process.env.DATABASE_URL!, {
  max: 10,
  idle_timeout: 20,
  connect_timeout: 10,
});

export const db = drizzle(client, { schema });
client rules:
    create once per process
    reuse connection pool
    configure max connections based on database capacity
    close client in shutdown hook when needed

select#

import { eq } from 'drizzle-orm';
import { db } from './db';
import { orders } from './schema';

export async function findOrderById(id: string) {
  const rows = await db
    .select()
    .from(orders)
    .where(eq(orders.id, id))
    .limit(1);

  return rows[0] ?? null;
}

insert#

export async function createOrder(input: {
  userId: string;
  amountCents: number;
}) {
  const rows = await db
    .insert(orders)
    .values({
      userId: input.userId,
      amountCents: input.amountCents,
    })
    .returning();

  return rows[0];
}

update#

export async function markOrderPaid(id: string) {
  const rows = await db
    .update(orders)
    .set({
      status: 'PAID',
      updatedAt: new Date(),
    })
    .where(eq(orders.id, id))
    .returning();

  return rows[0] ?? null;
}

transaction#

await db.transaction(async (tx) => {
  const [order] = await tx
    .insert(orders)
    .values({
      userId: 'user_1',
      amountCents: 1000,
    })
    .returning();

  await tx.insert(orderEvents).values({
    orderId: order.id,
    type: 'ORDER_CREATED',
  });
});
use transaction when:
    multiple writes must succeed or fail together
    state transition writes audit event
    balance / inventory / quota is updated

avoid:
    long external API call inside transaction
    user interaction inside transaction
    unbounded batch in one transaction

5. NestJS Integration#

provider token#

import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js';
import type * as schema from './schema';

export type Database = PostgresJsDatabase<typeof schema>;
export const DB = Symbol('DB');

database module#

import { Module } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as schema from './schema';
import { DB } from './database.provider';

@Module({
  providers: [
    {
      provide: DB,
      inject: [ConfigService],
      useFactory: (config: ConfigService) => {
        const client = postgres(config.getOrThrow<string>('DATABASE_URL'), {
          max: 10,
        });
        return drizzle(client, { schema });
      },
    },
  ],
  exports: [DB],
})
export class DatabaseModule {}

repository#

import { Inject, Injectable } from '@nestjs/common';
import { eq } from 'drizzle-orm';
import { DB } from '../database/database.provider';
import type { Database } from '../database/database.provider';
import { orders } from '../database/schema';

@Injectable()
export class OrdersRepository {
  constructor(@Inject(DB) private readonly db: Database) {}

  async findById(id: string) {
    const rows = await this.db
      .select()
      .from(orders)
      .where(eq(orders.id, id))
      .limit(1);

    return rows[0] ?? null;
  }
}
production note:
    keep DB provider in one module
    repositories should not create their own connections

6. Security Best Practices#

security:
    use parameterized query APIs
    avoid raw SQL string interpolation
    validate DTO before query
    use least privilege database user
    separate migration user from app runtime user when possible
    do not log DATABASE_URL
    use TLS to database in production

raw SQL#

import { sql } from 'drizzle-orm';

await db.execute(sql`select now()`);
raw SQL rule:
    use sql tagged template
    never concatenate user input into raw SQL
    wrap raw query in repository method

7. Migration / Reliability#

migration rules:
    generated SQL must be reviewed
    run migrations in CI/staging before production
    backward-compatible migration first
    deploy app after schema is compatible
    destructive migration needs backup and rollback plan
npm run db:generate
npm run db:migrate
classic safe migration:
    1. add nullable column
    2. deploy app writing both old and new fields
    3. backfill
    4. deploy app reading new field
    5. add not-null / drop old field after verification

8. Monitoring#

metrics:
    database_query_duration_seconds
    database_query_total
    database_error_total
    database_pool_used_connections
    database_pool_wait_count
    migration_duration_seconds

logs:
    query name / operation
    duration_ms
    rows affected
    error code
    request_id

alerts:
    query p95 high
    connection pool exhausted
    migration failed
    deadlock / serialization error spike

9. Hands-on#

start postgres#

services:
  postgres:
    image: postgres:16
    environment:
      POSTGRES_DB: orders
      POSTGRES_USER: order_app
      POSTGRES_PASSWORD: order_app_password
    ports:
      - "5432:5432"
    volumes:
      - postgres-data:/var/lib/postgresql/data

volumes:
  postgres-data:

environment#

export DATABASE_URL="postgres://order_app:order_app_password@localhost:5432/orders"

generate migration#

npm run db:generate
npm run db:migrate
npm run db:studio

10. Production Checklist#

schema:
    schema.ts reviewed
    indexes match query patterns
    generated SQL committed
    migration rollback plan exists

application:
    one database provider per process
    connection pool sized
    DTO validation enabled
    repositories hide Drizzle details

security:
    DATABASE_URL stored in secret manager
    runtime database user has least privilege
    TLS enabled for production database
    raw SQL reviewed

operations:
    migrations run before app traffic switch
    query duration monitored
    database errors alerted
    backup restore tested