nnestjs-drizzle-crud
Getting started

Quick start

A five-step walkthrough that takes a fresh Drizzle schema to a working CRUD controller in under five minutes.

This guide walks you through wiring a complete CRUD surface for a users table in a fresh NestJS + Drizzle project. By the end you'll have a controller exposing GET /users, GET /users/:id, POST /users, PUT /users/:id, and DELETE /users/:id, all backed by the SqlBaseCrudService base class.

Define your Drizzle schema

db/schema.ts
import { pgTable, serial, varchar, timestamp } from 'drizzle-orm/pg-core';

export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  name: varchar('name', { length: 100 }).notNull(),
  email: varchar('email', { length: 255 }).notNull().unique(),
  createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
  updatedAt: timestamp('updated_at', { withTimezone: true })
    .defaultNow()
    .notNull()
    .$onUpdate(() => new Date()),
  deletedAt: timestamp('deleted_at', { withTimezone: true }),
});

export const schema = { users };
export type User = typeof users.$inferSelect;

The createdAt / updatedAt / deletedAt columns are conventional — the package detects them by name. See Timestamps and Soft delete for alternatives.

Configure the module once

app.module.ts
import { Module } from '@nestjs/common';
import { DrizzleCrudModule } from 'nestjs-drizzle-crud';
import { schema } from './db/schema';
import { UsersModule } from './users/users.module';

@Module({
  imports: [
    DrizzleCrudModule.forRoot({
      dialect: 'postgresql',
      connectionString: process.env.DATABASE_URL,
      schema,
    }),
    UsersModule,
  ],
})
export class AppModule {}

The module creates and owns the connection, exposes it globally, and closes it on application shutdown.

Create an empty service

users/users.service.ts
import { SqlBaseCrudService } from 'nestjs-drizzle-crud';
import type { User } from '../db/schema';

export interface CreateUserDto { name: string; email: string }
export interface UpdateUserDto { name?: string; email?: string }
export interface UserFilterDto { name?: string; email?: string }

export class UsersService extends SqlBaseCrudService<
  User,
  CreateUserDto,
  UpdateUserDto,
  UserFilterDto
> {}

The service is an empty subclass. Do not inject the db, do not pass dialect or db to super(...), and do not add a @Inject('DRIZZLE_DB') constructor — forFeature constructs the service for you. The table is bound in forFeature, not in the service.

Bind the service to its table

users/users.module.ts
import { Module } from '@nestjs/common';
import { DrizzleCrudModule } from 'nestjs-drizzle-crud';
import { users } from '../db/schema';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

@Module({
  imports: [
    DrizzleCrudModule.forFeature([{ service: UsersService, table: users }]),
  ],
  controllers: [UsersController],
  providers: [UsersService],
})
export class UsersModule {}

Use it in a controller

users/users.controller.ts
import {
  Body,
  Controller,
  Delete,
  Get,
  Param,
  ParseIntPipe,
  Post,
  Put,
  Query,
} from '@nestjs/common';
import { CreateUserDto, UpdateUserDto, UsersService } from './users.service';

@Controller('users')
export class UsersController {
  constructor(private readonly users: UsersService) {}

  @Get()
  findAll(
    @Query('page') page = '1',
    @Query('limit') limit = '20',
  ) {
    return this.users.findAll({}, { page: +page, limit: +limit });
  }

  @Get(':id')
  find(@Param('id', ParseIntPipe) id: number) {
    return this.users.find(id);
  }

  @Post()
  create(@Body() dto: CreateUserDto) {
    return this.users.create(dto);
  }

  @Put(':id')
  update(
    @Param('id', ParseIntPipe) id: number,
    @Body() dto: UpdateUserDto,
  ) {
    return this.users.update(id, dto);
  }

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

@Param('id', ParseIntPipe) works because the id column is serial. If you switch to a uuid primary key, remove ParseIntPipe — see Primary keys.

What you got

After these five steps, UsersService exposes every CRUD method below, fully typed against User:

MethodReturnsNotes
find(id, options?)Promise<User | null>By primary key. Skips soft-deleted rows.
findOne(where, options?)Promise<User | null>Equality match.
findAll(filters?, pagination?, options?)Promise<{ data, total, page, limit }>Supports options.search. See Filtering and Pagination & sorting.
exists(id, options?)Promise<boolean>
count(filters?, options?)Promise<number>
create(dto, options?)Promise<User>Runs validateCreatebeforeCreate → insert → afterCreate.
update(id, dto, options?)Promise<User>Throws EntityNotFoundException if missing.
delete(id, options?)Promise<boolean>Hard delete.
softDelete(id, options?)Promise<boolean>Sets deleted_at.
restore(id, options?)Promise<User>Clears deleted_at.
massCreate(dtos, options?)Promise<User[]>Single transaction.
massUpdate(ids, dto, options?)Promise<User[]>Single transaction.
massSoftDelete(ids, options?)Promise<boolean>Single transaction.
massRestore(ids, options?)Promise<User[]>Single transaction.
massDelete(ids, options?)Promise<boolean>Single transaction.
fullTextSearch(term, columns, pagination?, options?)Promise<{ data, total, page, limit }>PostgreSQL only. Same envelope as findAll.
executeSqlTransaction(async (tx) => …)Promise<T>Run multiple operations in one transaction.

Next

  • Need relations, soft delete, or full-text search? Read the Guides.
  • Want every config knob? See Configuration.
  • Looking for a specific method signature? Jump to API reference.

On this page