Everybody Knows That Drizzle is the Word!

Published: (February 23, 2026 at 03:21 PM EST)
7 min read
Source: Dev.to

Source: Dev.to

Source: Dev.to – Everybody knows that “drizzle” is the word

Introduction

I’ve worked with plenty of ORMs and database tools over the years—JPA, Hibernate, Knex, Mongoose, Prisma. The list is long, and 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, but 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, and a simple refactor turns into a scavenger hunt for imports.

We’ll solve this with a few small building blocks:

  • Injection tokens – stable DI keys
  • Postgres provider – creates and owns the connection
  • Drizzle provider – builds the typed DB instance
  • DatabaseModule – thin 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:

  1. No service imports a database singleton from a file.
  2. All database access goes through one DI token.

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

PostgresProvider

- Input:  validated `database.url` from `ConfigService`.
- Output: a single Postgres client wrapper that owns shutdown.

DrizzleProvider

- Input:  the Postgres client and app env.
- Output: the Drizzle DB instance bound to **one DI token**.

DatabaseModule (Nest wiring)

- Guarantee: modules and services only depend on DI tokens, not on a file path.

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 writing any modules, we need stable injection tokens. These tokens are the only thing services should ever inject.

Install Drizzle and the PostgreSQL driver

pnpm add drizzle-orm@beta postgres

Note: The @beta tag is used here for the latest features. If you prefer fewer breaking changes, pin a stable version instead.

Define the tokens

Create a single file for the symbols and avoid redefining them elsewhere:

// database/database.constants.ts
export const DRIZZLE = Symbol('DRIZZLE');
export const POSTGRES_CLIENT = Symbol('POSTGRES_CLIENT');

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

Step 2: Implement the 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): PostgresClientProvider => {
    const { url } = configService.get('database', {
      infer: true,
    }) as DatabaseConfig;

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

Step 3: Implement the 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,
  ): DrizzleDB => {
    const { nodeEnv } = configService.get('app', {
      infer: true,
    }) as AppConfig;

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

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)

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

@Module({
  providers: [PostgresProvider, DrizzleProvider],
  exports: [DRIZZLE, POSTGRES_CLIENT],
})
export class DatabaseModule {}

Now any other module can simply import DatabaseModule and inject the tokens DRIZZLE or POSTGRES_CLIENT without ever touching a concrete file path.

Step 5: Use the injected DB instance in a service

// users/users.service.ts
import { Inject, Injectable } from '@nestjs/common';
import { DRIZZLE } from '../database/constants/database.constants';
import type { DrizzleDB } from '../database/types/database.type';

@Injectable()
export class UsersService {
  constructor(@Inject(DRIZZLE) private readonly db: DrizzleDB) {}

  async findAll() {
    return this.db.select().from(this.db.users).all();
  }
}

All database access now flows through a single, well‑typed DI token, keeping your codebase clean, testable, and fully Nest‑native.

Keep Your Database Initialization Boring and Isolated

A few rules to keep it simple:

  • 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 {}

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


Step 5 – Inject Drizzle Anywhere (No “random file” imports)

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

  1. Define a 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;
  2. Import the DatabaseModule wherever you need the client

    // any.module.ts
    import { Module } from '@nestjs/common';
    import { DatabaseModule } from '@database/database.module';
    
    @Module({
      imports: [DatabaseModule],
    })
    export class AnyModule {}
  3. Inject the token with the typed DB interface

    // 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) {}
    }

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

Takeaway – Keep Database Initialization Boring and Isolated

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 isn’t cleverness; it’s a database client that feels boring to use.

💡 Next post: Defining a typed Drizzle schema.

🔗 Code so far:

// src/db/client.ts
import { drizzle } from 'drizzle-orm/postgres';
import { Pool } from 'pg';
import { env } from '@/env';

const pool = new Pool({
  connectionString: env.DATABASE_URL,
});

export const db = drizzle(pool);

(Add additional code snippets as you progress.)

0 views
Back to Blog

Related posts

Read more »