nnestjs-drizzle-crud
Guides

Lifecycle hooks & validation

before/after hooks, validateCreate/validateUpdate, and DTO-to-entity mappers — all optional overrides.

The base class exposes a small set of protected methods you can override in your service. Each is optional and has a sensible default (no-op or pass-through).

Validation

class UsersService extends SqlBaseCrudService<User, CreateUserDto, UpdateUserDto> {
  protected async validateCreate(data: CreateUserDto): Promise<void> {
    if (!data.email.includes('@')) {
      throw new ValidationFailedException('Invalid email');
    }
  }

  protected async validateUpdate(id: number, data: UpdateUserDto): Promise<void> {
    if (data.email && !data.email.includes('@')) {
      throw new ValidationFailedException('Invalid email');
    }
  }
}

Both validateCreate and validateUpdate may return void or throw. Any throw is propagated to the caller and aborts the write — no insert, no update.

Validation runs before beforeCreate / beforeUpdate and before the SQL. Use it for fast, synchronous input validation. For heavier work (DB lookups, async checks), use beforeCreate / beforeUpdate instead — they'll run in the same code path but won't be confused with input shape validation.

DTO → entity mapping

The two mapper methods let you transform the DTO into the row to persist. By default, both return a shallow copy of the input.

class UsersService extends SqlBaseCrudService<User, CreateUserDto, UpdateUserDto> {
  protected mapCreateDtoToEntity(data: CreateUserDto) {
    return {
      ...data,
      email: data.email.toLowerCase().trim(),
      status: 'pending',
    };
  }

  protected mapUpdateDtoToEntity(data: UpdateUserDto) {
    const result: Record<string, any> = {};
    if (data.name !== undefined) result.name = data.name;
    if (data.email !== undefined) result.email = data.email.toLowerCase().trim();
    return result;
  }
}

Mappers are the right place to:

  • Normalize input (lowercase emails, trim whitespace).
  • Set fields the caller can't control (status: 'pending', createdBy).
  • Strip fields the DTO allows but the entity doesn't have.

before / after hooks

Each write has a before* and after* pair. They fire in this order:

validateCreate
  → mapCreateDtoToEntity
  → beforeCreate
  → SQL INSERT
  → afterCreate

afterCreate receives the freshly-created row. afterUpdate and afterRestore receive the updated row. afterDelete and afterSoftDelete receive the id.

class UsersService extends SqlBaseCrudService<User, CreateUserDto, UpdateUserDto> {
  protected async beforeCreate(data: CreateUserDto) {
    // Mutate the data in place, or return a new value.
    return { ...data, slug: slugify(data.name) };
  }

  protected async afterCreate(entity: User): Promise<void> {
    await this.events.emit('user.created', entity);
  }

  protected async afterUpdate(entity: User): Promise<void> {
    await this.events.emit('user.updated', entity);
  }

  protected async beforeDelete(id: number): Promise<void> {
    // e.g. publish a deletion intent, write an audit log.
  }
}

If beforeCreate returns a value, that value is used instead of the input. Returning undefined / not returning keeps the input as-is.

Skipping hooks

Pass options.hooks to skip a hook temporarily:

await users.create(dto, { hooks: { skipBefore: true, skipAfter: true } });

This is useful for migrations or admin scripts that shouldn't fire domain events.

Full hook list

HookArgumentReturnsFires on
validateCreate(data)CreateDtovoid or throwscreate(), massCreate() (per row)
validateUpdate(id, data)id, UpdateDtovoid or throwsupdate(), massUpdate() (per row)
mapCreateDtoToEntity(data)CreateDtoRecord<string, any>create(), massCreate() (per row)
mapUpdateDtoToEntity(data)UpdateDtoRecord<string, any>update(), massUpdate() (per row)
beforeCreate(data)CreateDtoCreateDto (or mutates)create(), massCreate() (per row)
afterCreate(entity)Tvoidcreate(), massCreate() (per row)
beforeUpdate(id, data)id, UpdateDtoUpdateDto (or mutates)update(), massUpdate() (per row)
afterUpdate(entity)Tvoidupdate(), massUpdate() (per row)
beforeDelete(id)idvoiddelete(), massDelete() (per row)
afterDelete(id)idvoiddelete(), massDelete() (per row)
beforeSoftDelete(id)idvoidsoftDelete(), massSoftDelete() (per row)
afterSoftDelete(id)idvoidsoftDelete(), massSoftDelete() (per row)
beforeRestore(id)idvoidrestore(), massRestore() (per row)
afterRestore(entity)Tvoidrestore(), massRestore() (per row)

All hooks are protected so they can't be called from outside the class. They're meant to be overridden in your service subclass.

Next

On this page