Drizzle
As an alternative to TypeORM, you can use Drizzle ORM with the @nestjs/drizzle package. Drizzle is a lightweight, TypeScript-first ORM. You declare tables with plain functions and write queries with a SQL-like query builder (or the relational query API for nested reads), and Drizzle infers every result type from the schema. There are no entity classes, decorators, or code generation steps.
To get started, install the required dependencies. This chapter uses PostgreSQL with the node-postgres driver, but Drizzle supports many other databases, including MySQL, SQLite, and Microsoft SQL Server, as well as serverless databases such as Neon, Turso, and Cloudflare D1. The steps in this chapter are the same for every database Drizzle supports: install the driver for your database, and import drizzle() from the matching entry point (e.g., drizzle-orm/mysql2 or drizzle-orm/libsql) instead of drizzle-orm/node-postgres.
$ npm install --save @nestjs/drizzle drizzle-orm@rc pg
$ npm install --save-dev drizzle-kit@rc @types/pg
Hint This chapter uses Drizzle ORM v1, currently published under therctag. The Drizzle documentation is written for this version.@nestjs/drizzlealso supports Drizzle v0.35 and later. With v0.x, only relations and relational queries work differently from this chapter (see Upgrading to v1).
Schema#
With Drizzle, you write the database schema in TypeScript. You declare each table with a function such as pgTable() and export it from a schema file. Let's define the users table:
import { boolean, integer, pgTable, text } from 'drizzle-orm/pg-core';
export const users = pgTable('users', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
firstName: text('first_name').notNull(),
lastName: text('last_name').notNull(),
isActive: boolean('is_active').notNull().default(true),
});
export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;
The $inferSelect and $inferInsert helpers derive the row types from the table definition, so there's no separate model class to keep in sync with the schema. For the available column types, see Schema declaration in the Drizzle documentation.
Registering the database#
Once the dependencies are installed and the schema is defined, import the DrizzleModule into the root AppModule:
import { Module } from '@nestjs/common';
import { DrizzleModule } from '@nestjs/drizzle';
import { drizzle } from 'drizzle-orm/node-postgres';
@Module({
imports: [
DrizzleModule.forRoot({
drizzle,
connection: process.env.DATABASE_URL!,
}),
],
})
export class AppModule {}
The drizzle option takes the drizzle() function of your driver. DrizzleModule calls it with the connection and the other Drizzle options once for each application instance. Every application, including each one your e2e tests create, therefore gets its own connection pool, which is closed when that application shuts down. Because you pass the function, every driver Drizzle supports works the same way, and TypeScript checks connection and the other options against that driver's drizzle() signature.
The forRoot() method supports the following options:
drizzle | The drizzle() function of your driver, e.g., the one exported by drizzle-orm/node-postgres |
connection | A connection string, or the driver's connection options (e.g., a pg pool configuration) |
db | A database instance that you created, used instead of drizzle and connection (see below) |
autoCloseConnection | If true, the database's client (db.$client) is closed when the application shuts down (default: true) |
Other options, such as relations, logger, or casing, are passed to drizzle() as well.
Hint Like the options of any module, theforRoot()options are evaluated when theAppModulefile is imported, soprocess.env.DATABASE_URLmust already be set at that point (e.g., by starting Node.js with the--env-file=.envflag). To read the connection string throughConfigServiceinstead, useforRootAsync(), described in the "Async configuration" section below.
Hint The client is closed when you callapp.close(), or when the process receives a termination signal if shutdown hooks are enabled. Pool-based clients (e.g.,node-postgres,postgres.js,mysql2) are closed withend(), and others (e.g.,better-sqlite3,libsql, PGlite) withclose(). For a database created with Drizzle'swithReplicas()function, the clients of the primary and all replica databases are closed (with Drizzle v0.44.6 and later; earlier versions don't expose the replicas, so only the primary's client is closed). A client registered under several names is closed only once.
Once the module is registered, you can inject the database anywhere in the project (without importing any modules) with the @InjectDrizzle() decorator. For example:
import { Injectable } from '@nestjs/common';
import { InjectDrizzle } from '@nestjs/drizzle';
import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
@Injectable()
export class AppService {
constructor(
@InjectDrizzle()
private readonly db: NodePgDatabase,
) {}
}
import { Injectable, Dependencies } from '@nestjs/common';
import { getDrizzleToken } from '@nestjs/drizzle';
@Injectable()
@Dependencies(getDrizzleToken())
export class AppService {
constructor(db) {
this.db = db;
}
}
To register a database that you create yourself, e.g., one that routes reads to replicas with Drizzle's withReplicas() function, pass it as db instead. Create it in a forRootAsync() factory, which runs once for each application:
import { Module } from '@nestjs/common';
import { DrizzleModule } from '@nestjs/drizzle';
import { drizzle } from 'drizzle-orm/node-postgres';
import { withReplicas } from 'drizzle-orm/pg-core';
@Module({
imports: [
DrizzleModule.forRootAsync({
useFactory: () => ({
db: withReplicas(drizzle(process.env.DATABASE_URL!), [
drizzle(process.env.REPLICA_DATABASE_URL!),
]),
}),
}),
],
})
export class AppModule {}
Warning You can also pass adbinstance toforRoot(), e.g., one exported by adbfile that your seed scripts import too. Such an instance is created once, when its file is first imported. Every application created fromAppModuleshares it, and the first application to shut down closes it, which breaks e2e test suites that create a new application for each test. In that case, setautoCloseConnectiontofalseand close the client yourself.
Queries#
Unlike the TypeORM integration, Drizzle has no forFeature() step. Drizzle tables are plain objects that you import wherever you build queries, and the database is registered globally, so any provider can inject it. Let's implement the UsersService:
import { Injectable } from '@nestjs/common';
import { InjectDrizzle } from '@nestjs/drizzle';
import { eq } from 'drizzle-orm';
import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
import { users, type NewUser, type User } from '../db/schema.js';
@Injectable()
export class UsersService {
constructor(
@InjectDrizzle()
private readonly db: NodePgDatabase,
) {}
findAll(): Promise<User[]> {
return this.db.select().from(users);
}
async findOne(id: number): Promise<User | undefined> {
const [user] = await this.db.select().from(users).where(eq(users.id, id));
return user;
}
async create(user: NewUser): Promise<User> {
const [createdUser] = await this.db.insert(users).values(user).returning();
return createdUser;
}
async remove(id: number): Promise<void> {
await this.db.delete(users).where(eq(users.id, id));
}
}
import { Injectable, Dependencies } from '@nestjs/common';
import { getDrizzleToken } from '@nestjs/drizzle';
import { eq } from 'drizzle-orm';
import { users } from '../db/schema.js';
@Injectable()
@Dependencies(getDrizzleToken())
export class UsersService {
constructor(db) {
this.db = db;
}
findAll() {
return this.db.select().from(users);
}
async findOne(id) {
const [user] = await this.db.select().from(users).where(eq(users.id, id));
return user;
}
async create(user) {
const [createdUser] = await this.db.insert(users).values(user).returning();
return createdUser;
}
async remove(id) {
await this.db.delete(users).where(eq(users.id, id));
}
}
A Drizzle query runs when it's awaited (or returned from an async function), and its result type is inferred from the columns it selects. The Drizzle documentation covers selecting, inserting, updating, and deleting rows in detail.
Relations#
Relations are associations between two or more tables, based on common fields, usually primary and foreign keys. Drizzle's relational queries use them to fetch nested data, such as users together with their photos, without writing joins by hand.
Let's add a photos table whose userId column references the users table:
export const photos = pgTable('photos', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
url: text().notNull(),
userId: integer('user_id')
.notNull()
.references(() => users.id),
});
You declare the relations for the whole schema at once, with the defineRelations() function. For example, each user can have many photos, and each photo belongs to one user:
import { defineRelations } from 'drizzle-orm';
import * as schema from './schema.js';
export const relations = defineRelations(schema, (r) => ({
users: {
photos: r.many.photos(),
},
photos: {
user: r.one.users({
from: r.photos.userId,
to: r.users.id,
}),
},
}));
To make the relations available to relational queries, pass them to drizzle() with the relations option:
DrizzleModule.forRoot({
drizzle,
connection: process.env.DATABASE_URL!,
relations,
}),
The database type now depends on the relations, so declare it once and reuse it wherever you inject the database:
import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
import type { relations } from './relations.js';
export type Database = NodePgDatabase<typeof relations>;
With that in place, the db.query API is fully typed:
@Injectable()
export class UsersService {
constructor(
@InjectDrizzle()
private readonly db: Database,
) {}
findOneWithPhotos(id: number) {
return this.db.query.users.findFirst({
where: { id },
with: { photos: true },
});
}
}
Hint Relations only drive relational queries. Foreign key constraints come from references() in the table definition. To learn more, see Relations in the Drizzle documentation.
Drizzle Transactions#
A database transaction is a unit of work that the database management system treats as a whole, independently of other transactions: either all of its changes are applied, or none of them are. To learn more, see Database transaction on Wikipedia.
To start a Drizzle transaction, call the transaction() method of the injected database. The callback receives a transaction object (tx) that exposes the same query API as the database:
async deactivate(id: number) {
await this.db.transaction(async (tx) => {
await tx.update(users).set({ isActive: false }).where(eq(users.id, id));
await tx.delete(photos).where(eq(photos.userId, id));
});
}
If the callback throws or calls tx.rollback(), the transaction is rolled back and transaction() rejects with the error. Otherwise, the transaction is committed, and transaction() resolves with the value the callback returned. Queries that belong to the transaction must run through tx, not through the injected database.
Hint To run code from several providers in one transaction without passingtxaround, propagate the transaction withAsyncLocalStorage. Thenestjs-clspackage provides a transactional plugin with a Drizzle adapter that does this.
Custom repositories#
Drizzle has no repository layer: you build queries from the injected database and the table definitions. To keep the queries for a table in one place, and to make the classes that use them easy to unit test, you can write a repository yourself as a regular provider:
import { Injectable } from '@nestjs/common';
import { InjectDrizzle } from '@nestjs/drizzle';
import { eq } from 'drizzle-orm';
import type { Database } from '../db/database.js';
import { users, type User } from '../db/schema.js';
@Injectable()
export class UsersRepository {
constructor(
@InjectDrizzle()
private readonly db: Database,
) {}
findActive(): Promise<User[]> {
return this.db.select().from(users).where(eq(users.isActive, true));
}
async deactivate(id: number): Promise<void> {
await this.db.update(users).set({ isActive: false }).where(eq(users.id, id));
}
}
Register UsersRepository in the providers array of UsersModule, and inject it into UsersService instead of the database. In unit tests, replace it with a custom provider whose useValue is an object implementing the methods your service calls, such as findActive().
Warning Repository methods query the injected database, not a transaction'stx. With a connection pool, they run on a different connection than a transaction started elsewhere withdb.transaction(), so they aren't part of it. To run them in a transaction, accepttxas a parameter, or propagate the transaction withnestjs-clsas described above.
Migrations#
Migrations incrementally update the database schema to keep it in sync with the application's data model, while preserving the existing data. To generate and run migrations, Drizzle provides a dedicated CLI, Drizzle Kit, which reads its settings from a drizzle.config.ts file in the root directory of your project:
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
dialect: 'postgresql',
schema: './src/db/schema.ts',
out: './drizzle',
dbCredentials: {
url: process.env.DATABASE_URL!,
},
});
With that file in place, npx drizzle-kit generate creates SQL migration files from your schema changes, and npx drizzle-kit migrate applies them to the database.
Drizzle Kit runs outside your Nest application, so migrations can't use dependency injection or other Nest-specific features. Drizzle Kit loads the .env file from the directory you run it in, so process.env.DATABASE_URL is available in drizzle.config.ts without ConfigModule. Variables already set in the environment take precedence over the file.
To apply pending migrations when the application starts instead, create the database in a forRootAsync() factory and apply them there. The factory can be async, and Nest doesn't create the providers that inject the database until it resolves:
import { drizzle } from 'drizzle-orm/node-postgres';
import { migrate } from 'drizzle-orm/node-postgres/migrator';
DrizzleModule.forRootAsync({
useFactory: async () => {
const db = drizzle(process.env.DATABASE_URL!, { relations });
await migrate(db, { migrationsFolder: './drizzle' });
return { db };
},
}),
Warning When several instances of your application start at the same time, each of them tries to apply the migrations. In such deployments, run drizzle-kit migrate as a separate step of your release process instead.
Multiple databases#
Some projects require multiple database connections, and the DrizzleModule supports this as well. Register each database with its own DrizzleModule import. In this case, every database except one must have a name:
@Module({
imports: [
DrizzleModule.forRoot({
drizzle,
connection: process.env.DATABASE_URL!,
relations,
}),
DrizzleModule.forRoot({
name: 'analytics',
drizzle,
connection: process.env.ANALYTICS_DATABASE_URL!,
}),
],
})
export class AppModule {}
Notice A database registered without anameis nameddefault. Don't register multiple databases without a name, or with the same name: they would share one injection token, and only one of them would be injected.
The databases don't have to share a driver: analytics could just as well be a SQLite file opened with the drizzle() function from drizzle-orm/better-sqlite3. To inject a database other than the default one, pass its name to the @InjectDrizzle() decorator:
@Injectable()
export class ReportsService {
constructor(
@InjectDrizzle('analytics')
private readonly analyticsDb: NodePgDatabase,
) {}
}
You can also inject any database into a factory provider with the getDrizzleToken() function, which returns the injection token of the database with the given name:
@Module({
providers: [
{
provide: ReportsService,
useFactory: (analyticsDb: NodePgDatabase) => {
return new ReportsService(analyticsDb);
},
inject: [getDrizzleToken('analytics')],
},
],
})
export class ReportsModule {}
Testing#
When unit testing an application, you usually want to avoid connecting to a database, to keep test suites independent and fast. However, your classes depend on the database injected with @InjectDrizzle(). To solve this, replace the database with a mock, using a custom provider. The getDrizzleToken() function returns the injection token of the default database (or of the database whose name you pass):
@Module({
providers: [
UsersService,
{
provide: getDrizzleToken(),
useValue: mockDb,
},
],
})
export class UsersModule {}
Nest now injects the mockDb object wherever a class requests the database with the @InjectDrizzle() decorator. Drizzle's query builder is chainable (e.g., select().from().where()), so a mock has to implement every method in the chain your code calls. For anything beyond simple queries, either move the queries into custom repositories and mock those, or run these tests against a real database.
Async configuration#
To pass the DrizzleModule options asynchronously instead of statically, use the forRootAsync() method. It supports several ways to provide the options.
One approach is a factory function. The factory behaves like any other asynchronous provider: it can be async, and it can inject dependencies through inject. It returns the same options as forRoot() (except name), either drizzle and connection or a db instance:
DrizzleModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
drizzle,
connection: configService.getOrThrow<string>('DATABASE_URL'),
relations,
}),
inject: [ConfigService],
});
Alternatively, you can use the useClass syntax:
DrizzleModule.forRootAsync({
imports: [ConfigModule],
useClass: DrizzleConfigService,
});
With this construction, DrizzleModule instantiates DrizzleConfigService, resolving its dependencies from the modules in imports, and calls its createDrizzleOptions() method to get the options. DrizzleConfigService must therefore implement the DrizzleOptionsFactory interface:
@Injectable()
class DrizzleConfigService implements DrizzleOptionsFactory {
constructor(private readonly configService: ConfigService) {}
createDrizzleOptions(): DrizzleModuleFactoryOptions {
return {
drizzle,
connection: this.configService.getOrThrow<string>('DATABASE_URL'),
relations,
};
}
}
HintcreateDrizzleOptions()receives the name of the database being registered (undefinedfor the default one), so a single class can configure several databases.
To reuse a provider from another module instead of instantiating DrizzleConfigService inside DrizzleModule, use the useExisting syntax:
DrizzleModule.forRootAsync({
imports: [DatabaseConfigModule],
useExisting: DrizzleConfigService,
});
This works like useClass, with one critical difference: DrizzleModule reuses the DrizzleConfigService instance exported by an imported module (here, DatabaseConfigModule) instead of creating a new one.

