Nestjs Drizzle
Configuration

forFeature

Bind a service to its table, and override per-entity configuration.

DrizzleCrudModule.forFeature(entities) is called inside each feature module. It binds one or more services to their tables and constructs them with the correct merged configuration.

Signature

DrizzleCrudModule.forFeature(entities: CrudFeature[]): DynamicModule

Where CrudFeature is:

interface CrudFeature {
  service: typeof SqlBaseCrudService;
  table: any;                 // Drizzle table
  config?: Partial<SqlCrudConfig>;  // per-entity overrides
}

Basic usage

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 {}

The module registers UsersService as a provider, passing it a fully-built SqlCrudConfig derived from the global forRoot defaults + any per-entity overrides you supply here.

Per-entity overrides

Anything in config overrides the project defaults for that entity. The type is CrudFeatureConfig: every SqlCrudConfig key is optional, and softDelete, timestamps, pagination and sql merge with the project defaults one level deep. pagination: { defaultLimit: 5 } keeps the project's maxLimit, and sql: { caseSensitive: true } keeps useReturning.

posts/posts.module.ts
DrizzleCrudModule.forFeature([
  {
    service: PostsService,
    table: posts,
    config: {
      primaryKey: 'id',
      softDelete: { enabled: true, column: 'deleted_at' },
      // Default ORDER BY for findAll() when the caller passes no sortBy.
      defaultSort: [
        { column: 'position', order: 'asc' },
        { column: 'created_at', order: 'desc' }, // tiebreaker
      ],
      relations: {
        author: { table: users, localKey: 'author_id', references: 'id' },
      },
    },
  },
])

What you can override

FieldEffect
primaryKeyKey of the primary key column (default 'id').
primaryKeyTypeDeprecated, no effect. The key type comes from the Drizzle column. See Primary keys.
softDelete{ enabled?: boolean; column?: string }. column is the key in your Drizzle table object, 'deleted_at' by default.
timestamps{ createdAt?: string; updatedAt?: string }. Keys of the timestamp columns, 'created_at' / 'updated_at' by default. timestamps: undefined turns them off for this entity.
pagination{ defaultLimit?: number; maxLimit?: number }. Per-entity override of the global defaults.pagination; a missing key keeps the project value.
defaultSortOrdered column list for findAll() when no sortBy is passed.
sqlPer-entity dialect tuning.
relationsBelongs-to, has-many and many-to-many relations, keyed by relation name. See Relations.
filterableColumns, sortableColumns, selectableColumns, searchableColumns, loadableRelationsWhat a caller may filter, sort, select, search and eager-load. Unset allows everything. See Allowlists.
writableColumnsThe columns create, update and upsert may write; anything else is a 400. Unset, the package ignores the columns it manages. See Allowlists.

Does a camelCase schema need extra config?

Yes, when your keys differ from the defaults. Settings name the key in the Drizzle table object, not the database column. For deletedAt: timestamp('deleted_at') and friends:

config: {
  softDelete: { column: 'deletedAt' },
  timestamps: { createdAt: 'createdAt', updatedAt: 'updatedAt' },
}

The service checks these keys at startup. A key that is not on the table stops the app with a message such as softDelete.column is "deleted_at", but the table has no column under that key. The column "deleted_at" is declared under the key "deletedAt"; set softDelete.column to "deletedAt".

Multiple entities in one feature module

DrizzleCrudModule.forFeature([
  { service: UsersService, table: users },
  { service: PostsService, table: posts },
  { service: CommentsService, table: comments, config: { /* ... */ } },
])

Each service is registered as its own provider and is independently constructed.

@CrudService decorator

You can also attach per-entity config to the service class itself with the @CrudService decorator. The forFeature config field still wins when both are set. The merge order is global defaults → decorator metadata → forFeature config.

import { CrudService } from 'nestjs-drizzle-crud';

@CrudService({
  primaryKey: 'id',
  softDelete: { enabled: true, column: 'deleted_at' },
})
export class TagsService extends SqlBaseCrudService<Tag> {}

The decorator is mostly a convenience for keeping the config next to the service definition. Use forFeature config for config that's data-driven or varies per feature module.

Verifying the merged config

The fully-built config for each entity is available in the DRIZZLE_CRUD_CONFIG DI token. Inject it in tests to verify the merge worked:

import { Inject } from '@nestjs/common';
import { DRIZZLE_CRUD_CONFIG } from 'nestjs-drizzle-crud';

@Injectable()
export class ConfigInspector {
  constructor(@Inject(DRIZZLE_CRUD_CONFIG) public readonly cfg: any) {}
}

The injected DRIZZLE_CRUD_CONFIG is the normalized global config (the result of applying defaults). It does not include the per-entity overrides. To inspect a single entity's resolved config, use DRIZZLE_DB + read it off the constructed service instance.

Next

On this page