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
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
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
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
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
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:
| Method | Returns | Notes |
|---|---|---|
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 validateCreate → beforeCreate → 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.