Using Drizzle ORM

Introduction

  • This is a next-generation TypeScript-first ORM (Object-Relational Mapping) library designed to interact with relational databases (PostgreSQL, MySQL, SQLite) in a lightweight, Type-safe and high-performance manner.
  • Unlike traditional ORMs (such as Prisma, TypeORM, Hibernate) that hide SQL behind complex abstraction layers, Drizzle follows the philosophy If you know SQL, you know Drizzle. It serves as both an ORM and a powerful Query Builder.
alt text

Advantages

  • Absolute TypeScript & Type-safe standard (100% Type-Safe): Schema is defined directly using TypeScript functions. When you write queries, Drizzle automatically infers the return type without needing cumbersome code generation steps.
  • Extremely high performance & super lightweight (Zero Overhead): Drizzle has no heavy dependencies and does not use background engines (such as Rust binaries in Prisma). Its ultra-small package size helps optimize cold start times, making it ideal for Serverless and Edge Environments (Next.js, Vercel, Cloudflare Workers, Supabase).
  • Syntax close to native SQL: Drizzle API mirrors the exact structure of SQL (select(), from(), where(), leftJoin()) statements. This helps developers easily control the exact SQL statements generated without performance surprises (like N+1 query issues).
  • Supports both SQL-like API and Relational API: You can flexibly choose to write pure SQL-style queries or use Relational Queries (db.query.users.findMany(...)) syntax when needing simple nested relational data retrieval.
  • Transparent Migration management (Drizzle Kit): The included drizzle-kit tool automatically compares Schemas and generates Migration files as raw .sql files instead of custom formats. This makes it easy to track, test and customize SQL before applying it to the Database.
  • Predictable queries (Exactly 1 SQL Query): Each query in Drizzle generates exactly one SQL statement sent to the Database, reducing round-trip costs and optimizing processing speed.

Use cases

  • Projects built with TypeScript / JavaScript (suitable for NextJS, Remix, NestJS).
  • Applications deployed on Serverless / Edge Functions requiring fast startup.
  • Developers who like the flexibility of SQL while still wanting tight IntelliSense and Type-safety.

Detail

First, install the following packages

bun add drizzle-orm drizzle-kit

Add the following scripts to package.json

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

Create a simple schema like this drizzle-orm/schema.ts

import {
  bigserial,
  integer,
  pgTable,
  text,
  timestamp,
  varchar,
} from 'drizzle-orm/pg-core'

export const products = pgTable('products', {
  id: bigserial('id', {mode: 'number'}).primaryKey(),
  name: varchar('name', {length: 255}).notNull(),
  description: text('description'),
  categoryId: integer('category_id').notNull(),
  createdAt: timestamp('created_at').defaultNow().notNull(),
})

You can see that using Drizzle ORM makes defining table structures very simple, almost like working with a real Database

Next, define the database connection information from Drizzle ORM to Postgres as follows drizzle-orm/index.ts

import {drizzle, type NodePgDatabase} from 'drizzle-orm/node-postgres'
import {Pool} from 'pg'
import * as schema from './schema'

export type DrizzleDB = NodePgDatabase<typeof schema>

export const DRIZZLE_TOKEN = 'DRIZZLE_DEV'

export const createPool = () => {
  return new Pool({connectionString: process.env.DATABASE_URL})
}

export const createDrizzle = () => {
  const pool = createPool()
  return drizzle(pool, {schema})
}

export const drizzleProvider = {
  provide: DRIZZLE_TOKEN,
  useFactory: createDrizzle,
}

Create service/drizzle.service.ts

import {Injectable, OnModuleDestroy} from '@nestjs/common'
import {Pool} from 'pg'
import {createDrizzle, createPool, type DrizzleDB} from 'src/drizzle-orm'

@Injectable()
export class DrizzleService implements OnModuleDestroy {
  private readonly pool: Pool
  public readonly db: DrizzleDB

  constructor() {
    this.pool = createPool()
    this.db = createDrizzle()
  }

  async onModuleDestroy() {
    await this.pool.end()
  }
}

Create service/products.service.ts

import {Inject, Injectable, NotFoundException} from '@nestjs/common'
import {eq} from 'drizzle-orm'
import {DRIZZLE_TOKEN, type DrizzleDB} from 'src/drizzle-orm'
import * as schema from 'src/drizzle-orm/schema'
import {CreateProductDto, UpdateProductDto} from 'src/dto/product.dto'
import {DrizzleService} from './drizzle.service'

@Injectable()
export class ProductsService {
  constructor(
    @Inject(DRIZZLE_TOKEN) private readonly db: DrizzleDB,
    private readonly drizzle: DrizzleService
  ) {}

  async create(dto: CreateProductDto) {
    const [result] = await this.db
      .insert(schema.products)
      .values(dto)
      .returning()
    return result
  }

  async findAll() {
    return this.drizzle.db.select().from(schema.products)
  }

  async findOne(id: number) {
    const [result] = await this.db
      .select()
      .from(schema.products)
      .where(eq(schema.products.id, id))

    if (!result) {
      throw new NotFoundException(`Product with ID ${id} not found`)
    }
    return result
  }

  async update(id: number, dto: UpdateProductDto) {
    const [updated] = await this.db
      .update(schema.products)
      .set(dto)
      .where(eq(schema.products.id, id))
      .returning()

    if (!updated) {
      throw new NotFoundException(`Product with ID ${id} not found`)
    }
    return updated
  }

  async remove(id: number) {
    const [deleted] = await this.db
      .delete(schema.products)
      .where(eq(schema.products.id, id))
      .returning()

    if (!deleted) {
      throw new NotFoundException(`Product with ID ${id} not found`)
    }
    return {success: true, deletedId: deleted.id}
  }
}

I provide 2 ways to use Drizzle by using @Inject(DRIZZLE_TOKEN) and DrizzleService, both of which work similarly

Create file dto/product.dto.ts

import {PartialType} from '@nestjs/mapped-types'
import {
  IsInt,
  IsNotEmpty,
  IsOptional,
  IsString,
  MaxLength,
} from 'class-validator'

export class CreateProductDto {
  @IsString()
  @IsNotEmpty()
  @MaxLength(255)
  name: string

  @IsString()
  @IsOptional()
  description?: string

  @IsInt()
  @IsNotEmpty()
  categoryId: number
}

export class UpdateProductDto extends PartialType(CreateProductDto) {}

Next is controller/products.controller.ts to define simple CRUD APIs

import {
  Body,
  Controller,
  Delete,
  Get,
  Param,
  ParseIntPipe,
  Patch,
  Post,
} from '@nestjs/common'
import {CreateProductDto, UpdateProductDto} from 'src/dto/product.dto'
import {ProductsService} from 'src/service/products.service'

@Controller('products')
export class ProductsController {
  constructor(private productsService: ProductsService) {}

  @Post()
  create(@Body() createProductDto: CreateProductDto) {
    return this.productsService.create(createProductDto)
  }

  @Get()
  findAll() {
    return this.productsService.findAll()
  }

  @Get(':id')
  findOne(@Param('id', ParseIntPipe) id: number) {
    return this.productsService.findOne(id)
  }

  @Patch(':id')
  update(
    @Param('id', ParseIntPipe) id: number,
    @Body() updateProductDto: UpdateProductDto
  ) {
    return this.productsService.update(id, updateProductDto)
  }

  @Delete(':id')
  remove(@Param('id', ParseIntPipe) id: number) {
    return this.productsService.remove(id)
  }
}

Modify app.module.ts

import {Module} from '@nestjs/common'
import {ConfigModule} from '@nestjs/config'
import {ProductsController} from './controller/products.controller'
import {drizzleProvider} from './drizzle-orm'
import {DrizzleService} from './service/drizzle.service'
import {ProductsService} from './service/products.service'

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: '.env',
    }),
  ],
  controllers: [
    ProductsController,
  ],
  providers: [
    drizzleProvider,
    DrizzleService,
    ProductsService,
  ],
})
export class AppModule {}

Next, there are 2 ways to apply the schema from Drizzle to Postgres:

  • Use the bun db:push command directly
  • Use the bun db:generate command to create a .sql file, then use bun db:migrate to execute the migration into the Database

Option 1

$ bun db:push
$ drizzle-kit push
No config path provided, using default 'drizzle.config.ts'
Reading config file '~/drizzle.config.ts'
Using 'pg' driver for database querying
[✓] Pulling schema from database...
[✓] Changes applied

Option 2

After that, you can check directly in the Database that the table was created successfullyalt text

You can use this command to generate a migration file

$ bun db:generate
$ drizzle-kit generate
No config path provided, using default 'drizzle.config.ts'
Reading config file '~/drizzle.config.ts'
1 tables
products 5 columns 0 indexes 0 fks

[✓] Your SQL migration file ➜ drizzle/0000_slow_mephistopheles.sql 🚀

The result will generate the drizzle/0000_slow_mephistopheles.sql file containing table information to migrate as follows

CREATE TABLE "products" (
 "id" bigserial PRIMARY KEY NOT NULL,
 "name" varchar(255) NOT NULL,
 "description" text,
 "category_id" integer NOT NULL,
 "created_at" timestamp DEFAULT now() NOT NULL
);

After checking that the script has no issues, you can use the bun db:migrate command to apply changes to the Database

Start the project to verify that the APIs are working properlyalt textalt textalt textalt textalt text

Happy coding!

See more articles here.

Comments

Popular posts from this blog

All Practice Series

Understanding React Server Component

Kubernetes Deployment for Zero Downtime

Sitemap

Deploying a NodeJS Server on Google Kubernetes Engine

React Practice Series

Helm for beginer - Deploy nginx to Google Kubernetes Engine

Docker Practice Series

A Handy Guide to Using Dynamic Import in JavaScript

DevOps Practice Series