nnestjs-drizzle-crud
Reference

For AI agents

Condensed, accurate package facts for code generation. Prefer these over guessing.

This page is a tight, fact-only summary designed to be read by code-generation tools. Prefer the rules and gotchas below over re-deriving them from the rest of the docs.

Package

  • Name: nestjs-drizzle-crud
  • Version: 3.3.0
  • License: MIT
  • Peers: @nestjs/common, @nestjs/core, drizzle-orm (>=0.28.0 <1.0.0), reflect-metadata. Optional: postgres (PG) / mysql2 (MySQL).

Setup is two steps and no per-service connection wiring

  1. DrizzleCrudModule.forRoot({ dialect, connectionString | db, schema, defaults }) once in AppModule.
  2. DrizzleCrudModule.forFeature([{ service, table, config? }]) in each feature module.

A service is an empty subclass: do NOT inject the db or pass dialect/db

class XService extends SqlBaseCrudService<X, CreateXDto, UpdateXDto, XFilterDto> {}

Rules / gotchas

  • Generics order: SqlBaseCrudService<Entity, CreateDto, UpdateDto, FilterDto>. All but Entity default to Partial<Entity>.
  • Do not add an @Inject('DRIZZLE_DB') constructor, forFeature constructs the service for you. Adding a constructor that calls super({...}) is a legacy/manual pattern and is unnecessary.
  • The table is passed in forFeature, not in the service.
  • If tables lack timestamp / soft-delete columns, set defaults: { softDelete: false, timestamps: false }, else inserts will reference non-existent columns.
  • findAll returns { data, total, page, limit }. Not a bare array.
  • Filter operators live inside an object value: { age: { gte: 18 } }. like / ilike require caller-supplied % wildcards; a bare string is an exact, case-insensitive (when caseSensitive === false) match, % / _ / \ in the value are literal data, never wildcards.
  • findAll and count accept options.search: { term, columns, mode? }. Default mode is 'ilike' and joins cross-column contains checks with OR; mode: 'fullText' is PostgreSQL-only. Search is applied to both data and count queries.
  • delete / softDelete return boolean; update / restore return the entity and throw EntityNotFoundException (→ 404) when missing. On Postgres, restore() returns the row via RETURNING rather than re-fetching it.
  • All exceptions extend @nestjs/common's HttpException, so Nest's default filter maps them to status codes automatically: 404 (not found), 409 (unique violation), 400 (validation / bad input), 500 (connection / transaction). Custom filters can instanceof them.
  • Relations: declare in forFeature config.relations, then eager-load with options.relations: ['relName'] (a dotted path nests: ['states', 'states.cities'], ['state.country'], and the parent path is pulled in automatically).
    • { table, localKey, references? } is belongs-to: LEFT JOINed into the main query, resolves to an object or null.
    • { table, type: 'hasMany', foreignKey, parentKey?, orderBy? } resolves to an array.
    • { table, type: 'manyToMany', through, throughLocalKey, throughForeignKey, references?, parentKey?, orderBy? } resolves to an array; the join-table key never appears in the result.
    • Nested relations go in a relations map on the relation itself.
    • Every collection and every nested level costs exactly ONE extra query for the whole result set, never one per row. A page of 50 countries with their states and each state's cities is 4 queries: data, count, states, cities.
  • A collection is never joined. Filter via findAll({ relName: { col: value } }) with the same operators: belongs-to uses the join, has-many and many-to-many compile to parentKey IN (SELECT ... WHERE ...), so total and pagination stay correct. Filters are one level deep; go through the intermediate table's own columns, e.g. state.country_id.
  • A relation missing its required keys throws when the service is constructed (nested included, with the full path in the message).
  • options.select narrows the projection and accepts dotted relation paths at any depth: ['id', 'name', 'region.code', 'states.cities.name']. Unknown names are ignored. When every entry is dotted the base row is returned in full. The parent key, the child foreign key and any key a nested level needs are re-added automatically, because the grouping needs them.
  • The package ships an MCP server, npx nestjs-drizzle-crud-mcp (stdio), with list_docs, get_doc and search_docs over AGENT.md / README.md / CHANGELOG.md. Rules templates for every agent: AGENTS.template.md and CLAUDE.template.md.
  • Primary keys: primaryKey (default 'id') + primaryKeyType ('serial' | 'bigserial' | 'int' | 'bigint' | 'uuid') per entity in forFeature config. UUID works on Postgres via RETURNING; with uuid keys, route params are strings. Don't use ParseIntPipe.
  • Full-text search is PostgreSQL-only.
  • Bulk methods are transactional. A failure throws BulkOperationException and rolls back the entire operation.
  • executeSqlTransaction(async (tx) => ...) opens a transaction; pass { transaction: tx } in options to any CRUD method to enlist it.

Quick reference

Configure

DrizzleCrudModule.forRoot({
  dialect: 'postgresql',
  connectionString: process.env.DATABASE_URL,
  schema,
});

Service

class UsersService extends SqlBaseCrudService<User> {}

Bind

DrizzleCrudModule.forFeature([{ service: UsersService, table: users }]);

Filter

await service.findAll({
  status: 'active',                        // exact
  role: ['admin', 'editor'],               // IN
  age: { gte: 18, lt: 65 },                // operators
  name: { ilike: 'jo%' },                  // pattern (caller wildcards)
  deletedAt: { isNull: true },
});
await service.findAll(
  { status: 'active' },
  { page: 1, limit: 20 },
  { search: { term: 'john', columns: ['name', 'email'] } },
);

Eager load relations

await service.findAll({}, { page: 1, limit: 20 }, { relations: ['state'] });

Public exports (alphabetical)

and, asc, BaseCrudSpecHelper, BulkOperationException, CrudFeature,
CrudService, DatabaseConnectionException, desc, DRIZZLE_CONNECTION,
DRIZZLE_CRUD_CONFIG, DRIZZLE_DB, DrizzleConnection, DrizzleCrudConfig,
DrizzleCrudModule, DuplicateEntityException, EntityInUseException, eq,
EntityNotFoundException, ForeignKeyViolationException, gt, gte, ICrudService,
ilike, inArray, isNotNull, isNull, like, lt, lte, ne,
or, PrimaryKeyType, RelationConfig, RelationsConfig, SortColumn, SortOrder,
sql, SqlBaseCrudService, SqlCrudConfig, SqlDialect, SqlOperationOptions,
TestCrudFactory, TransactionException, ValidationFailedException

Anti-patterns: do not generate these

// ❌ wrong: db is not injected
class UsersService extends SqlBaseCrudService<User> {
  constructor(@Inject(DRIZZLE_DB) private db: any) {
    super({ dialect: 'postgresql' as const, db, table: users });
  }
}

// ❌ wrong: super() takes no arguments
class UsersService extends SqlBaseCrudService<User> {
  constructor() {
    super(/* nothing */);
  }
}

// ❌ wrong: table is bound in forFeature, not the service
class UsersService extends SqlBaseCrudService<User, /* ... */, users> {}

// ❌ wrong: returning a bare array from a controller
@Get()
async findAll() {
  return this.users.findAll({});  // returns { data, total, page, limit }
}

// ❌ wrong: ParseIntPipe on a UUID primary key
@Get(':id')
find(@Param('id', ParseIntPipe) id: number) { /* ... */ }

Sources of truth

When in doubt, prefer:

  1. The TypeScript types, import type { ... } from 'nestjs-drizzle-crud' is the canonical definition.
  2. The package README on npm.
  3. The repo at github.com/myviliha/viliha-drizzle-crud.
  4. The Changelog. Release-by-release behavior changes since 3.0.0.

This page summarizes (1) and (2) and may lag minor patch releases. If a generated snippet disagrees with the TypeScript types, trust the types.

On this page