DEV Community

Cover image for Everybody Knows That Drizzle is the Word!
Ruben Alvarado
Ruben Alvarado

Posted on

Everybody Knows That Drizzle is the Word!

I’ve worked with plenty of ORMs and database tools over the years—JPA, Hibernate, Knex, Mongoose, Prisma. The list is long. There’s always a new “revolutionary” tool. So when I first heard about Drizzle, I was skeptical.

I was happy with Prisma, until I had to integrate with a Supabase Postgres database. After a week fighting the client generation workflow, I gave Drizzle a shot. The integration felt boring in the best way: it was as smooth as working with native Postgres.

Then I plugged it into NestJS, and the experience changed. Supabase felt easy. Nest introduced friction—because the real problem isn’t Drizzle or Nest. It’s the glue code in between.

In this post, we’ll make Drizzle feel Nest-native using providers.

The real problem: your DB client becomes a hidden dependency

Drizzle is minimal by design. NestJS is modular by design. The friction starts the moment you try to make “minimal” fit inside a DI container.

It usually begins innocently: one file creates the client, another reads DATABASE_URL, and a service imports a singleton from some db.ts file in a random folder.

It works. Until it doesn’t.

Soon the database client becomes a hidden dependency. Tests that should be pure unit tests suddenly require wiring. A simple refactor turns into a scavenger hunt for imports.

We’ll get there with a few small building blocks:

  • Injection tokens (stable DI keys).

  • A Postgres provider (creates and owns the connection).

  • A Drizzle provider (builds the typed DB instance).

  • A thin DatabaseModule (Nest wiring only).

The plan: a thin DatabaseModule

Here’s the blueprint we’re aiming for. If we do this right, two things become true:

  • No service imports a database singleton from a file.

  • All database access goes through one DI token.

We’ll get there with one module boundary and two providers, each with a single responsibility:

  1. PostgresProvider
- Input: validated `database.url` from `ConfigService`.
- Output: a single Postgres client wrapper that owns shutdown.
Enter fullscreen mode Exit fullscreen mode
  1. DrizzleProvider
- Input: the Postgres client and app env.        
- Output: the Drizzle DB instance bound to **one DI token**.
Enter fullscreen mode Exit fullscreen mode
  1. DatabaseModule (Nest wiring)
 - Guarantee: modules and services only depend on DI tokens, not on a file path.
Enter fullscreen mode Exit fullscreen mode

That separation keeps Nest concerns (modules, DI, lifecycle) out of your Drizzle initialization logic.

Next, we’ll build it in five steps.

Step 1: Create dedicated injection tokens

Before we write any modules, we need stable injection tokens. These tokens are the only thing services should ever inject.

First, install Drizzle and the postgres driver:

pnpm add drizzle-orm@beta postgres
Enter fullscreen mode Exit fullscreen mode

💡 Note: I’m using @beta here. If you prefer fewer breaking changes, pin a stable version instead.

Now define a symbol token in a single place, and don’t redefine it anywhere else:

// database/database.constants.ts
export const DRIZZLE = Symbol('DRIZZLE');
export const POSTGRES_CLIENT = Symbol('POSTGRES_CLIENT');
Enter fullscreen mode Exit fullscreen mode

With tokens in place, we can implement the Postgres and Drizzle providers.

Step 2: Implement postgres provider

// database/providers/postgres.provider.ts
import { Logger, OnApplicationShutdown } from '@nestjs/common';
import postgres from 'postgres';
import { POSTGRES_CLIENT } from '../constants/database.constants';
import { ConfigService } from '@nestjs/config';
import { DatabaseConfig } from '@app/config/types/database-config.type';

export class PostgresClientProvider implements OnApplicationShutdown {
  private readonly logger = new Logger(PostgresClientProvider.name);

  constructor(public readonly client: postgres.Sql) {}

  async onApplicationShutdown(signal?: string) {
    this.logger.log(`Closing database connection (signal: ${signal})`);
    await this.client.end();
  }
}

export const PostgresProvider = {
  provide: POSTGRES_CLIENT,
  inject: [ConfigService],
  useFactory: (
    configService: ConfigService<{ database: DatabaseConfig }>,
  ): PostgresClientProvider => {
    const { url } = configService.get<DatabaseConfig>('database', {
      infer: true,
    }) as DatabaseConfig;

    const client = postgres(url, { prepare: false });
    return new PostgresClientProvider(client);
  },
};
Enter fullscreen mode Exit fullscreen mode

Step 3: Implement Drizzle provider

// database/providers/drizzle.provider.ts
import { ConfigService } from '@nestjs/config';
import { drizzle } from 'drizzle-orm/postgres-js';

import { DRIZZLE, POSTGRES_CLIENT } from '../constants/database.constants';
import { DrizzleDB } from '../types/database.type';
import { PostgresClientProvider } from './postgres.provider';
import { AppConfig } from '@app/config/types/app-config.types';

import * as schema from '../schema';

export const DrizzleProvider = {
  provide: DRIZZLE,
  inject: [POSTGRES_CLIENT, ConfigService],
  useFactory: (
    postgresProvider: PostgresClientProvider,
    configService: ConfigService<{ app: AppConfig }>,
  ): DrizzleDB => {
    const { nodeEnv } = configService.get<AppConfig>('app', {
      infer: true,
    }) as AppConfig;

    return drizzle({
      client: postgresProvider.client,
      schema,
      logger: nodeEnv === 'development',
    });
  },
};
Enter fullscreen mode Exit fullscreen mode

In the next steps we’ll define a single DrizzleDB type (derived from your schema) and reuse it everywhere.

Step 4: Wire it up in DatabaseModule (Nest-only concerns)

This is where Nest becomes useful. We register both providers and export one token that the rest of the app can inject.

A few rules to keep it boring:

  • The module wires providers that read config.

  • The module creates the Drizzle instance once.

  • Services only inject DRIZZLE.

// database/database.module.ts
import { Global, Module } from '@nestjs/common';
import { DRIZZLE } from './constants/database.constants';
import { DrizzleProvider } from './providers/drizzle.provider';
import { PostgresProvider } from './providers/postgres.provider';

@Global()
@Module({
  providers: [PostgresProvider, DrizzleProvider],
  exports: [DRIZZLE],
})
export class DatabaseModule {}
Enter fullscreen mode Exit fullscreen mode

At this point, your database client is no longer a hidden dependency. It’s a first-class provider with a stable token.

Next, we’ll inject it from any module without importing “random files”.

Step 5: Inject Drizzle anywhere (no imports from “random files”)

Now the payoff: modules import a module, and services inject a token.

First, define a single strongly typed DB type derived from your schema:

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

export type DrizzleDB = PostgresJsDatabase<typeof schema>;
Enter fullscreen mode Exit fullscreen mode

Then use that type anywhere you inject DRIZZLE:

// any.module.ts
import { Module } from '@nestjs/common';
import { DatabaseModule } from '@database/database.module';

@Module({
  imports: [DatabaseModule],
})
export class AnyModule {}
Enter fullscreen mode Exit fullscreen mode
// example.service.ts
import { Inject, Injectable } from '@nestjs/common';
import { DRIZZLE } from '@database/database.constants';
import { DrizzleDB } from '@database/types/database.type';

@Injectable()
export class ExampleService {
  constructor(@Inject(DRIZZLE) private readonly db: DrizzleDB) {}
}
Enter fullscreen mode Exit fullscreen mode

Because the provider was created with schema, DrizzleDB stays in sync with your tables and queries.

Takeaway: keep database initialization boring and isolated

If you treat your database client as infrastructure and give it a dedicated module boundary, three things happen:

  • Config stays centralized (and validated).

  • Services stay clean (they inject a token, not a file import).

  • Future work gets easier (schema, migrations, and tests build on a stable foundation).

The goal is not cleverness. The goal is a database client that feels boring to use.

💡 Next post: Defining a typed Drizzle schema.


🔗 Code so far: https://github.com/RubenOAlvarado/finance-api/tree/v0.3.0

Top comments (0)