ut blocking the application bootstrap phase.
import { Module } from '@nestjs/common';
import { DrizzleModule } from 'nest-drizzle-native';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { drizzle } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';
@Module({
imports: [
ConfigModule.forRoot(),
DrizzleModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => {
const pool = new Pool({
connectionString: config.get('DATABASE_URL'),
max: 20,
idleTimeoutMillis: 30000,
});
return {
client: drizzle(pool),
isGlobal: true,
};
},
}),
],
})
export class AppModule {}
Architecture Rationale: Async initialization prevents startup failures from blocking the NestJS lifecycle. Connection pooling is configured explicitly to match workload characteristics. The isGlobal flag eliminates the need to import the module in every feature module, reducing boilerplate while maintaining strict dependency boundaries.
Step 2: Repository Definition with Explicit Queries
Repositories should remain thin wrappers around Drizzle’s query builder. Avoid active record patterns; instead, expose methods that return typed results and preserve SQL transparency.
import { Injectable } from '@nestjs/common';
import { DrizzleRepository, InjectDatabase } from 'nest-drizzle-native';
import { eq, and } from 'drizzle-orm';
import { ledger_entries, customer_accounts } from './schema';
import type { TransferPayload, AccountRecord } from './types';
@DrizzleRepository()
export class LedgerRepository {
constructor(@InjectDatabase() private readonly db: any) {}
async recordTransfer(payload: TransferPayload): Promise<AccountRecord> {
const [entry] = await this.db
.insert(ledger_entries)
.values({
originId: payload.originId,
destinationId: payload.destinationId,
value: payload.value,
state: 'pending',
})
.returning();
return entry;
}
async getBalanceByAccount(accountId: string): Promise<number> {
const result = await this.db
.select({ total: ledger_entries.value })
.from(ledger_entries)
.where(eq(ledger_entries.accountId, accountId));
return result.reduce((sum, row) => sum + Number(row.total), 0);
}
}
Architecture Rationale: The @DrizzleRepository() decorator registers the class as a NestJS provider while binding it to the current database context. Explicit insert and select calls maintain query visibility. Type safety is preserved through Drizzle’s schema definitions. This approach prevents the "ORM magic" anti-pattern where query optimization becomes impossible due to hidden abstraction layers.
Step 3: Transaction Routing via Context Decorators
The core architectural advantage comes from CLS-backed transaction routing. Instead of passing tx objects, services use a decorator that automatically binds the execution context to a transaction instance.
import { Injectable } from '@nestjs/common';
import { TransactionContext, InjectTransaction } from 'nest-drizzle-native';
import { LedgerRepository } from './ledger.repository';
import { ComplianceService } from './compliance.service';
import type { TransferPayload } from './types';
@Injectable()
export class SettlementService {
constructor(
private readonly ledger: LedgerRepository,
private readonly compliance: ComplianceService,
) {}
@TransactionContext()
async processSettlement(payload: TransferPayload) {
const record = await this.ledger.recordTransfer(payload);
await this.compliance.registerEvent({
category: 'SETTLEMENT_INITIATED',
traceId: record.id,
executedAt: new Date(),
});
return { completed: true, settlementId: record.id };
}
}
Architecture Rationale: @TransactionContext() hooks into the CLS lifecycle. When the method executes, the integration checks for an active transaction. If none exists, it opens one and binds it to the async context. All subsequent repository calls within the same execution chain automatically receive the transaction instance. This eliminates manual tx passing while preserving atomicity. The decorator approach aligns with NestJS’s declarative style, making transaction boundaries explicit and auditable.
Pitfall Guide
-
Async Boundary Leakage in CLS Context
Explanation: CLS relies on Node.js async hooks. If you spawn untracked promises (e.g., void someAsyncCall() or setTimeout), the transaction context detaches, causing queries to run outside the transaction.
Fix: Always await repository calls within transactional methods. Use AsyncLocalStorage explicitly if you must fire-and-forget, or route background tasks through a separate non-transactional service.
-
Over-Abstracting Drizzle Query Builders
Explanation: Wrapping Drizzle queries in generic CRUD methods defeats the purpose of using a SQL-first ORM. You lose type inference, query optimization visibility, and database-specific features.
Fix: Keep repositories focused on domain-specific operations. Use Drizzle’s select, insert, and update directly. Reserve generic abstractions for cross-cutting concerns like soft deletes or audit logging.
-
Ignoring Connection Pool Configuration
Explanation: Default pool settings often mismatch production workloads. Under-provisioned pools cause connection starvation; over-provisioned pools exhaust database memory.
Fix: Configure max, min, and idleTimeoutMillis based on concurrent request volume and query duration. Monitor pg_stat_activity to validate pool utilization. Tune pool size relative to CPU cores and expected I/O wait times.
-
Testing Without Transaction Isolation
Explanation: Unit tests that hit the real database without transaction rollback leave residual data, causing flaky tests and state pollution.
Fix: Wrap test suites in a global transaction using @nestjs-cls/transactional test utilities. Roll back automatically after each test case. Use in-memory SQLite or Dockerized PostgreSQL for integration tests.
-
Mixing Synchronous and Asynchronous Repository Calls
Explanation: Drizzle’s query builder returns promises. Calling repository methods without await breaks CLS context binding and causes race conditions.
Fix: Enforce async/await patterns across all data access layers. Use TypeScript strict mode and ESLint rules (@typescript-eslint/await-thenable) to catch missing awaits. Implement custom decorators that validate async execution in CI pipelines.
-
Misusing Named Connections in Multi-Tenant Setups
Explanation: When scaling to multiple databases, developers often hardcode connection names or fail to scope repositories correctly, leading to cross-tenant data leakage.
Fix: Use DrizzleModule.forFeature({ connectionName: 'tenant_db' }) explicitly. Validate connection routing in middleware or guards. Implement tenant-aware repository factories that resolve the correct client at runtime.
-
Forgetting to Handle Rollback Exceptions
Explanation: CLS transaction decorators automatically roll back on thrown errors, but developers sometimes catch exceptions and suppress them, leaving transactions in an inconsistent state.
Fix: Never swallow errors inside @TransactionContext() methods. If you must handle errors, re-throw them or use explicit try/catch with manual rollback logic outside the decorator scope. Log rollback events for audit compliance.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Single database, low concurrency | DrizzleModule.forRoot() with static config | Simpler setup, faster bootstrap | Low infrastructure cost |
| Multi-environment deployment | forRootAsync() with ConfigService | Dynamic credential injection, zero hardcoded secrets | Moderate config management overhead |
| Cross-service atomic operations | @TransactionContext() decorator | Automatic CLS routing, eliminates manual tx passing | Negligible runtime overhead |
| Multi-tenant architecture | Named connections with forFeature() | Strict data isolation, prevents cross-tenant queries | Higher connection pool allocation |
| High-throughput batch processing | Manual transaction management outside CLS | Avoids context overhead, enables bulk optimizations | Increased code complexity |
Configuration Template
// database.module.ts
import { Module } from '@nestjs/common';
import { DrizzleModule } from 'nest-drizzle-native';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { drizzle } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';
@Module({
imports: [
DrizzleModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => {
const pool = new Pool({
connectionString: config.getOrThrow('DATABASE_URL'),
max: Number(config.get('DB_POOL_MAX', 20)),
idleTimeoutMillis: 30000,
});
return { client: drizzle(pool), isGlobal: true };
},
}),
],
exports: [DrizzleModule],
})
export class DatabaseModule {}
// settlement.repository.ts
import { DrizzleRepository, InjectDatabase } from 'nest-drizzle-native';
import { eq } from 'drizzle-orm';
import { settlement_records } from './schema';
@DrizzleRepository()
export class SettlementRepository {
constructor(@InjectDatabase() private readonly db: any) {}
async createSettlement(data: any) {
const [record] = await this.db.insert(settlement_records).values(data).returning();
return record;
}
async findByReference(ref: string) {
const [record] = await this.db.select().from(settlement_records).where(eq(settlement_records.reference, ref)).limit(1);
return record;
}
}
Quick Start Guide
- Install dependencies:
npm install nest-drizzle-native drizzle-orm pg @nestjs-cls/transactional
- Register the module asynchronously in your root module using
DrizzleModule.forRootAsync() with a configured connection pool.
- Create a repository class decorated with
@DrizzleRepository() and inject the database client using @InjectDatabase().
- Wrap service methods that require atomicity with
@TransactionContext() to enable automatic transaction routing across injected dependencies.
- Run integration tests using the package’s testing utilities to verify transaction isolation and rollback behavior.