Skip to main content

Drizzle ORM with NestJS and PostgreSQL in 2026: A Production-Ready Setup

Sakar Khadka
14 min read
Drizzle ORM with NestJS and PostgreSQL in 2026: A Production-Ready Setup

TL;DR — This guide wires NestJS → DatabaseService → postgres.js → Drizzle ORM → PostgreSQL with type-safe schemas, Drizzle Kit migrations, connection pooling, graceful shutdown, and process.env for config. No @nestjs/config. No magic globals. Just a setup you can ship and scale.

If you're building a serious NestJS backend in 2026, choosing the database layer early matters more than it seems.

You don't just need an ORM that can create tables and run queries. You want something that is:

RequirementWhy it matters
Type-safeCatch schema/query bugs at compile time
FastLow overhead at runtime
Migration-friendlyVersion-controlled schema changes
TestableEasy to mock via NestJS DI
Production-readyPooling, timeouts, graceful shutdown
Simple configRead env vars directly — no extra abstraction

For my NestJS projects, one setup that checks those boxes particularly well is Drizzle ORM + PostgreSQL.

In this guide, we'll build a clean integration using pnpm, PostgreSQL, Drizzle ORM, and postgres.js — with configuration loaded straight from process.env.

What you'll learn#

  • How to structure a scalable core/database layer in NestJS
  • How to connect Drizzle to PostgreSQL via postgres.js
  • How to generate and apply migrations with Drizzle Kit
  • How to read DATABASE_URL directly from process.env (no @nestjs/config)
  • How to add health checks, graceful shutdown, and a production-ready folder layout

Final stack#

LayerTool
FrameworkNestJS
ORMDrizzle ORM
Driverpostgres.js
DatabasePostgreSQL
MigrationsDrizzle Kit
Package managerpnpm
Configprocess.env + .env

The goal isn't just to get a database connection working — it's to create a database foundation you won't have to completely rewrite six months later.

Why Drizzle ORM with NestJS?#

NestJS doesn't force you to use a particular ORM.

That's actually a good thing.

NestJS is database-agnostic and can work with general-purpose database libraries and ORMs through its dependency injection system.

Drizzle fits naturally into this model.

Instead of hiding the database behind a huge abstraction layer, Drizzle keeps your SQL concepts relatively close to the application while providing strong TypeScript inference.

You define your schema in TypeScript:

import {
  pgTable,
  uuid,
  varchar,
  timestamp,
} from 'drizzle-orm/pg-core';
 
export const users = pgTable('users', {
  id: uuid('id').defaultRandom().primaryKey(),
 
  email: varchar('email', {
    length: 255,
  }).notNull().unique(),
 
  name: varchar('name', {
    length: 100,
  }).notNull(),
 
  createdAt: timestamp('created_at', {
    withTimezone: true,
  })
    .defaultNow()
    .notNull(),
});

And Drizzle turns that schema into SQL migrations and strongly typed queries.

That's the part I really like.

You get TypeScript's developer experience without completely forgetting that PostgreSQL exists.

What we're building#

Our final architecture will look like this:

NestJS


DatabaseService


postgres.js


Drizzle ORM


PostgreSQL

And the migration workflow:

Drizzle Schema


Drizzle Kit


SQL Migration


PostgreSQL

This keeps application runtime concerns separate from migration tooling.


1. Create the NestJS project#

If you're starting from scratch:

pnpm dlx @nestjs/cli new my-api

Choose the options you want for your project.

For a modern TypeScript backend, I prefer ESM for new projects unless you have a dependency that specifically requires CommonJS.

Once the project is created:

cd my-api

2. Install Drizzle and PostgreSQL#

For this setup, we'll use postgres.js as the PostgreSQL driver.

Install the runtime packages:

pnpm add drizzle-orm postgres

Then install Drizzle Kit:

pnpm add -D drizzle-kit

Drizzle's current PostgreSQL documentation supports postgres.js as one of its PostgreSQL drivers and provides the same general schema/configuration/migration workflow used here.

You don't need pg for this particular setup.


3. Add PostgreSQL configuration#

Create a .env file:

NODE_ENV=development
PORT=4000
 
DATABASE_URL=postgresql://postgres:password@localhost:5432/my_api

For production, use your actual managed PostgreSQL or server connection string.

Don't commit .env.

Your repository should contain:

.env
.env.example

The .env.example can contain:

NODE_ENV=development
PORT=4000
DATABASE_URL=

4. Create the database structure#

I like keeping infrastructure concerns inside a core directory in NestJS applications.

src/
├── core/
│   ├── config/
│   │   └── env.ts
│   ├── database/
│   │   ├── database.module.ts
│   │   ├── database.service.ts
│   │   ├── database.types.ts
│   │   ├── schema/
│   │   │   └── index.ts
│   │   └── relations/
│   │       └── index.ts
│   └── health/

├── modules/
│   ├── users/
│   └── auth/

├── app.module.ts
└── main.ts

The important distinction is:

core/database

Technical database infrastructure
 
modules/users

Business logic

Your database connection shouldn't become a business module.


5. Configure Drizzle Kit#

Create this file at the project root:

drizzle.config.ts

Use:

import 'dotenv/config';
 
import { defineConfig } from 'drizzle-kit';
 
export default defineConfig({
  schema: './src/core/database/schema/*.schema.ts',
 
  out: './drizzle',
 
  dialect: 'postgresql',
 
  dbCredentials: {
    url: process.env.DATABASE_URL!,
  },
 
  strict: true,
  verbose: true,
});

Drizzle's current configuration documentation uses drizzle.config.ts to define the PostgreSQL dialect, schema location, migration output directory, and database credentials.

Notice that we're loading .env directly here:

import 'dotenv/config';

That's intentional.

drizzle.config.ts is executed by Drizzle Kit, not by NestJS.

Your Nest application can have its own configuration system.


6. Create your first schema#

Create:

src/core/database/schema/users.schema.ts
import {
  pgTable,
  uuid,
  varchar,
  timestamp,
} from 'drizzle-orm/pg-core';
 
export const users = pgTable('users', {
  id: uuid('id')
    .defaultRandom()
    .primaryKey(),
 
  email: varchar('email', {
    length: 255,
  })
    .notNull()
    .unique(),
 
  name: varchar('name', {
    length: 100,
  }).notNull(),
 
  createdAt: timestamp('created_at', {
    withTimezone: true,
  })
    .defaultNow()
    .notNull(),
 
  updatedAt: timestamp('updated_at', {
    withTimezone: true,
  })
    .defaultNow()
    .notNull(),
});

Then export it:

// src/core/database/schema/index.ts
 
export * from './users.schema.js';

Because we're using ESM, notice the .js extension in the import.


7. Create the Drizzle database type#

Create:

src/core/database/database.types.ts
import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js';
 
import * as schema from './schema/index.js';
 
export type AppDatabase =
  PostgresJsDatabase<typeof schema>;

This gives your Nest services a fully typed Drizzle database instance.


8. Create the DatabaseService#

Now we connect PostgreSQL to Drizzle.

Create:

src/core/database/database.service.ts
import {
  Injectable,
  OnModuleDestroy,
  OnModuleInit,
} from '@nestjs/common';
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
 
import * as schema from './schema/index.js';
import type { AppDatabase } from './database.types.js';
 
function getDatabaseUrl(): string {
  const url = process.env.DATABASE_URL;
 
  if (!url) {
    throw new Error(
      'DATABASE_URL is not set. Add it to your .env file.',
    );
  }
 
  return url;
}
 
@Injectable()
export class DatabaseService
  implements OnModuleInit, OnModuleDestroy
{
  private readonly client: postgres.Sql;
 
  readonly db: AppDatabase;
 
  constructor() {
    const databaseUrl = getDatabaseUrl();
 
    this.client = postgres(databaseUrl, {
      max: 20,
      idle_timeout: 30,
      connect_timeout: 10,
    });
 
    this.db = drizzle({
      client: this.client,
      schema,
    });
  }
 
  async onModuleInit(): Promise<void> {
    await this.client`SELECT 1`;
 
    console.log('Database connected ✅');
  }
 
  async onModuleDestroy(): Promise<void> {
    await this.client.end();
 
    console.log('Database connection closed.');
  }
}

The important part is:

this.client = postgres(databaseUrl);
 
this.db = drizzle({
  client: this.client,
  schema,
});

Drizzle's current PostgreSQL documentation explicitly supports creating a postgres.js client and passing it into Drizzle.


9. Why use a DatabaseService?#

You could technically do this:

const db = drizzle(...);

in a random file and import it everywhere.

I wouldn't recommend that in a NestJS application.

Nest has dependency injection for a reason.

Instead:

NestJS

DatabaseService

Drizzle

Your services can then simply request:

constructor(
  private readonly database: DatabaseService,
) {}

This makes the database easier to:

  • Mock in tests
  • Replace later
  • Configure
  • Monitor
  • Shut down gracefully
  • Extend with transactions
  • Extend with tenant context
  • Manage in one place

Nest supports custom providers and application lifecycle hooks for managing external resources such as database connections.


10. Create DatabaseModule#

Create:

src/core/database/database.module.ts
import { Global, Module } from '@nestjs/common';
 
import { DatabaseService } from './database.service.js';
 
@Global()
@Module({
  providers: [DatabaseService],
  exports: [DatabaseService],
})
export class DatabaseModule {}

The @Global() decorator means you don't need to import DatabaseModule into every feature module.

Import it once in AppModule.


11. Add it to AppModule#

import { Module } from '@nestjs/common';
 
import { DatabaseModule } from './core/database/database.module.js';
 
@Module({
  imports: [DatabaseModule],
})
export class AppModule {}

No ConfigModule. No ConfigService. The database layer reads env vars directly.


12. Load environment variables with process.env#

We keep configuration simple: read from process.env and fail fast when required values are missing.

Load .env in main.ts#

import 'dotenv/config';
 
import { NestFactory } from '@nestjs/core';
 
import { AppModule } from './app.module.js';
 
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
 
  app.enableShutdownHooks();
 
  const port = Number(process.env.PORT) || 4000;
 
  await app.listen(port);
}
 
bootstrap();

Install dotenv if you haven't already:

pnpm add dotenv

Note: Drizzle Kit already loads .env via import 'dotenv/config' in drizzle.config.ts. Your Nest app does the same in main.ts. One source of truth, no extra config package.

Optional: centralize env access#

If you want a single place to validate env vars, create a small helper — still using process.env directly underneath:

src/core/config/env.ts
function requireEnv(name: string): string {
  const value = process.env[name];
 
  if (!value) {
    throw new Error(
      `Missing required environment variable: ${name}`,
    );
  }
 
  return value;
}
 
export const env = {
  nodeEnv: process.env.NODE_ENV ?? 'development',
  port: Number(process.env.PORT) || 4000,
  databaseUrl: requireEnv('DATABASE_URL'),
} as const;

Then in DatabaseService:

import { env } from '../config/env.js';
 
const databaseUrl = env.databaseUrl;

This gives you a clean, explicit configuration pipeline:

.env

dotenv/config (main.ts)

process.env

DatabaseService

postgres.js

Drizzle ORM

PostgreSQL

No @nestjs/config. No nested config keys. Just environment variables — the way most production Node.js apps actually work.


13. Generate your first migration#

Add these scripts to package.json:

{
  "scripts": {
    "db:generate": "drizzle-kit generate",
    "db:migrate": "drizzle-kit migrate",
    "db:push": "drizzle-kit push",
    "db:studio": "drizzle-kit studio",
    "db:check": "drizzle-kit check"
  }
}

Now run:

pnpm db:generate

Then:

pnpm db:migrate

Drizzle Kit generates migration SQL from your TypeScript schema, while migrate applies those migrations to PostgreSQL.

You'll end up with something like:

drizzle/
├── 0000_initial.sql
└── meta/

Commit these migration files to Git.


14. push vs generate#

This distinction is important.

For rapid local experimentation:

pnpm db:push

Drizzle documents push as a convenient way to quickly synchronize schema changes with a development database without managing migration files manually.

For a production application:

pnpm db:generate
pnpm db:migrate

I recommend the migration workflow once your schema becomes important.

A good workflow is:

Development

Change schema

pnpm db:push

Test

Finalize schema

pnpm db:generate

Commit migration

Production

pnpm db:migrate

15. Query Drizzle from a NestJS service#

For example:

src/modules/users/users.service.ts
import { Injectable } from '@nestjs/common';
import { eq } from 'drizzle-orm';
 
import { DatabaseService } from '../../core/database/database.service.js';
import { users } from '../../core/database/schema/users.schema.js';
 
@Injectable()
export class UsersService {
  constructor(
    private readonly database: DatabaseService,
  ) {}
 
  async findAll() {
    return this.database.db
      .select()
      .from(users);
  }
 
  async findById(id: string) {
    const [user] =
      await this.database.db
        .select()
        .from(users)
        .where(eq(users.id, id))
        .limit(1);
 
    return user ?? null;
  }
}

That's the beauty of the setup.

Your feature doesn't know how PostgreSQL connections are created.

It only knows:

this.database.db

16. Add relations as your application grows#

Don't put every relation into one giant schema file.

For a larger application:

database/
├── schema/
│   ├── users.schema.ts
│   ├── gyms.schema.ts
│   ├── branches.schema.ts
│   ├── memberships.schema.ts
│   └── attendance.schema.ts

└── relations/
    ├── users.relations.ts
    ├── gyms.relations.ts
    ├── memberships.relations.ts
    └── index.ts

For example:

export const usersRelations = relations(
  users,
  ({ many }) => ({
    memberships: many(memberships),
  }),
);

This becomes especially useful once your application has multiple related business domains.


17. Keep schema and business logic separate#

This is one architectural rule I strongly recommend.

Don't do this:

users/
├── users.schema.ts
├── users.service.ts
└── users.controller.ts

if you're deliberately treating the database as infrastructure.

Instead:

core/
└── database/
    └── schema/
        └── users.schema.ts
 
modules/
└── users/
    ├── users.controller.ts
    ├── users.service.ts
    └── users.module.ts

The difference is:

Schema
  = How data is stored
 
Service
  = What the application does with that data

That separation becomes extremely valuable in larger systems.


18. Graceful database shutdown#

We already implemented:

async onModuleDestroy() {
  await this.client.end();
}

But make sure Nest receives shutdown signals.

In main.ts:

const app =
  await NestFactory.create(AppModule);
 
app.enableShutdownHooks();
 
await app.listen(port);

Nest's lifecycle system provides shutdown hooks specifically for cleanup work such as closing connections gracefully.


19. Add database readiness checks#

You should distinguish between:

/health

and:

/health/ready

/health#

Answers:

Is the application process alive?

It shouldn't require PostgreSQL.

/health/ready#

Answers:

Can this application actually serve requests?

Here you can check PostgreSQL:

await this.database.db.execute(
  sql`SELECT 1`,
);

So your infrastructure becomes:

GET /health

Application alive
 
 
GET /health/ready

Application alive
     +
PostgreSQL reachable

This is much more useful when deploying to Docker, Kubernetes, Render, VPS infrastructure, or other production environments.


20. Production project structure#

After the project grows, I recommend something like:

src/
├── core/
│   ├── config/
│   │   └── env.ts
│   │
│   ├── database/
│   │   ├── database.module.ts
│   │   ├── database.service.ts
│   │   ├── database.types.ts
│   │   │
│   │   ├── schema/
│   │   │   ├── users.schema.ts
│   │   │   ├── gyms.schema.ts
│   │   │   ├── branches.schema.ts
│   │   │   ├── memberships.schema.ts
│   │   │   └── attendance.schema.ts
│   │   │
│   │   └── relations/
│   │       ├── users.relations.ts
│   │       ├── gyms.relations.ts
│   │       └── index.ts
│   │
│   ├── health/
│   └── observability/

├── common/
│   ├── decorators/
│   ├── guards/
│   ├── interceptors/
│   ├── filters/
│   └── pipes/

├── modules/
│   ├── auth/
│   ├── users/
│   ├── gyms/
│   ├── branches/
│   ├── memberships/
│   └── attendance/

├── app.module.ts
└── main.ts
 
drizzle/
├── 0000_initial.sql
├── 0001_add_gyms.sql
├── 0002_add_memberships.sql
└── meta/
 
drizzle.config.ts
.env
.env.example
package.json

This keeps the architecture understandable even when the project becomes large.


Production checklist#

Before calling the integration production-ready, I'd want these boxes checked:

  • PostgreSQL
  • Drizzle ORM
  • postgres.js
  • TypeScript types
  • NestJS dependency injection
  • Centralized DatabaseService
  • Connection pooling
  • Connection timeout
  • Graceful shutdown
  • Drizzle Kit
  • Version-controlled migrations
  • Separate schema files
  • Separate relations
  • process.env configuration (no @nestjs/config)
  • Database readiness check
  • Development push workflow
  • Production generate + migrate workflow

And importantly, don't add complexity like RLS, AsyncLocalStorage, tenant-specific connections, or repository abstractions until the application's actual authorization and multi-tenant requirements justify them. Those are useful advanced tools, but "future-proof" doesn't mean adding every possible abstraction on day one.

The final stack#

NestJS

   ├── process.env + dotenv

   ├── DatabaseModule
   │       │
   │       └── DatabaseService
   │               │
   │               └── postgres.js
   │                       │
   │                       └── Drizzle ORM
   │                               │
   │                               └── PostgreSQL

   ├── HealthModule

   └── Business Modules

           ├── Auth
           ├── Users
           ├── Gyms
           ├── Branches
           ├── Memberships
           └── Attendance

That's the setup I'd use as the foundation for a serious NestJS backend in 2026: Drizzle ORM + postgres.js + PostgreSQL, wired through NestJS dependency injection, configured with process.env, and migrated with Drizzle Kit. Simple, type-safe, and production-ready.