Skip to content

NestJS: Hooks

Overview

Hooks handle side effects around CRUD operations — notifications, cache invalidation, audit logging, data enrichment. For business logic use service overrides, for response transformation use Data Mapper.

Creating hooks

Extend RelayerHooks<TEntity, TEntities>. All hook methods are optional, support sync and async, and receive fully typed arguments:

import { Injectable, Logger } from '@nestjs/common';
import {
RelayerHooks,
type AggregateOptions,
type FirstOptions,
type ManyOptions,
type RequestContext,
type Where,
type WhereOptions,
} from '@relayerjs/nestjs-crud';
import { PostEntity, type EM } from '../entities';
@Injectable()
export class PostHooks extends RelayerHooks<PostEntity, EM> {
private readonly logger = new Logger(PostHooks.name);
constructor(
private readonly events: EventEmitter2,
private readonly cache: CacheManager,
) {
super();
}
// ... implement any hooks you need
}

Available hooks

beforeCreate

Called before inserting a new record. Return modified data to change what gets saved.

async beforeCreate(
data: Partial<PostEntity>,
ctx: RequestContext,
): Promise<Partial<PostEntity> | void> {
data.slug = slugify(data.title!);
return data;
}

afterCreate

Called after a record is inserted.

async afterCreate(entity: PostEntity, ctx: RequestContext): Promise<void> {
this.logger.log(`Post created: ${entity.id} - ${entity.title}`);
await this.events.emit('post.created', entity);
}

beforeUpdate

Called before updating. Receives both the data to update and the where clause. Return modified data.

async beforeUpdate(
data: Partial<PostEntity>,
where: Where<PostEntity, EM>,
ctx: RequestContext,
): Promise<Partial<PostEntity> | void> {
data.updatedAt = new Date();
return data;
}

afterUpdate

Called after a record is updated.

async afterUpdate(entity: PostEntity, ctx: RequestContext): Promise<void> {
await this.cache.del(`posts:${entity.id}`);
}

beforeDelete

Called before deleting.

async beforeDelete(
where: Where<PostEntity, EM>,
ctx: RequestContext,
): Promise<void> {
this.logger.log(`Deleting post with where: ${JSON.stringify(where)}`);
}

afterDelete

Called after a record is deleted.

async afterDelete(entity: PostEntity, ctx: RequestContext): Promise<void> {
await this.cache.del(`posts:${entity.id}`);
await this.events.emit('post.deleted', entity);
}

beforeFind

Called before findMany (list endpoint). Receives the full query options.

async beforeFind(
options: ManyOptions<PostEntity, EM>,
ctx: RequestContext,
): Promise<void> {
this.logger.log(`Finding posts with options: ${JSON.stringify(options)}`);
}

afterFind

Called after findMany. Return a modified list to filter or transform results.

async afterFind(
entities: PostEntity[],
ctx: RequestContext,
): Promise<PostEntity[] | void> {
return entities.filter((e) => !e.isArchived);
}

beforeFindOne

Called before findFirst (detail endpoint).

async beforeFindOne(
options: FirstOptions<PostEntity, EM>,
ctx: RequestContext,
): Promise<void> {
this.logger.log(`Finding post: ${JSON.stringify(options)}`);
}

afterFindOne

Called after findFirst. Return a modified entity.

async afterFindOne(
entity: PostEntity,
ctx: RequestContext,
): Promise<PostEntity | void> {
// e.g. track view count
}

beforeCount

Called before count queries.

async beforeCount(
options: WhereOptions<PostEntity, EM>,
ctx: RequestContext,
): Promise<void> {
this.logger.log(`Counting posts`);
}

beforeAggregate

Called before aggregate queries.

async beforeAggregate(
options: AggregateOptions<PostEntity, EM>,
ctx: RequestContext,
): Promise<void> {
this.logger.log(`Aggregating: ${JSON.stringify(options)}`);
}

afterAggregate

Called after aggregate. Return modified result.

async afterAggregate(
result: unknown,
ctx: RequestContext,
): Promise<unknown | void> {
this.logger.log(`Aggregate result: ${JSON.stringify(result)}`);
}

Summary table

HookArgumentsCan modify?
beforeCreate(data: Partial<TEntity>, ctx)Return modified data
afterCreate(entity: TEntity, ctx)
beforeUpdate(data: Partial<TEntity>, where: Where, ctx)Return modified data
afterUpdate(entity: TEntity, ctx)
beforeDelete(where: Where, ctx)
afterDelete(entity: TEntity, ctx)
beforeFind(options: ManyOptions, ctx)
afterFind(entities: TEntity[], ctx)Return modified list
beforeFindOne(options: FirstOptions, ctx)
afterFindOne(entity: TEntity, ctx)Return modified entity
beforeCount(options: WhereOptions, ctx)
beforeAggregate(options: AggregateOptions, ctx)
afterAggregate(result: unknown, ctx)Return modified result

All option types (Where, ManyOptions, FirstOptions, WhereOptions, AggregateOptions) are generic over <TEntity, TEntities>.

Registration

Register in module providers and reference in @CrudController:

posts.controller.ts
@CrudController<PostEntity, EM>({
model: PostEntity,
hooks: PostHooks,
})
export class PostsController extends RelayerController<PostEntity, EM> { ... }
// posts.module.ts
@Module({
providers: [PostsService, PostHooks],
})
export class PostsModule {}

Hooks are resolved via NestJS DI — constructor injection works.