Lifecycle Hooks
Every MongoDB service exposes a set of protected _before* / _after* methods that are called around each database operation. Override them in your subclass to inject business logic without wrapping every call site.
The base implementations do nothing — there is no need to call super.
Available hooks
| Hook | When it runs |
|---|---|
_beforeCreate(command) | Before inserting a document |
_afterCreate(command, result) | After insert and fetch-back |
_beforeReplace(command) | Before replacing a document |
_afterReplace(command, result) | After replace and fetch-back |
_beforeUpdate(command) | Before a single-document update |
_afterUpdate(command, result) | After update and optional fetch-back |
_beforeUpdateMany(command) | Before a multi-document update |
_afterUpdateMany(command, affected) | After a multi-document update |
_beforeDelete(command) | Before deleting a single document |
_afterDelete(command, affected) | After deleting a single document |
_beforeDeleteMany(command) | Before deleting multiple documents |
_afterDeleteMany(command, affected) | After deleting multiple documents |
Accessing the command object
The command argument passed to every hook contains the full context of the operation:
command.crud // 'create' | 'read' | 'replace' | 'update' | 'delete'
command.method // 'create' | 'update' | 'updateMany' | 'delete' | ...
command.documentId // _id of the target document (where applicable)
command.input // the patch/input data — can be mutated in _before* hooks
command.options // the operation options — can be mutated in _before* hooks
Mutating command.input in a _before* hook is the standard way to inject computed fields (timestamps, defaults, derived values) before the data reaches the database.
Real-world examples
Multi-tenant blog platform
A blogging platform where every post belongs to a tenant. _beforeCreate stamps ownership and sets defaults; _afterDelete publishes an event so other services can react:
export class PostsService extends MongoCollectionService<Post> {
constructor(
db: Db,
private readonly events: EventBus,
) {
super(Post, {
db,
documentFilter: (_, _this) => ({ tenantId: _this.context.tenantId }),
});
}
protected override async _beforeCreate(command: MongoEntityService.CreateCommand<Post>) {
command.input.tenantId = this.context.tenantId;
command.input.authorId = this.context.userId;
command.input.createdAt = new Date();
command.input.updatedAt = new Date();
command.input.status = 'draft';
}
protected override async _beforeUpdate(command: MongoEntityService.UpdateOneCommand<Post>) {
if (command.input) command.input.updatedAt = new Date();
}
protected override async _afterDelete(
command: MongoEntityService.DeleteCommand<Post>,
affected: number,
) {
if (affected) {
await this.events.publish('post.deleted', {
tenantId: this.context.tenantId,
postId: command.documentId,
});
}
}
}
E-commerce inventory guard
Validate stock availability and lock in the unit price before an order line is persisted; decrement stock atomically after a successful insert:
export class OrderLinesService extends MongoCollectionService<OrderLine> {
constructor(
db: Db,
private readonly inventory: InventoryService,
) {
super(OrderLine, { db });
}
protected override async _beforeCreate(command: MongoEntityService.CreateCommand<OrderLine>) {
const { productId, quantity } = command.input;
const product = await this.inventory.for(this.context).get(productId);
if (product.stock < quantity) {
throw new UnprocessableEntityError(
`Insufficient stock for product ${productId}: ` +
`requested ${quantity}, available ${product.stock}`,
);
}
command.input.unitPrice = product.price;
command.input.createdAt = new Date();
}
protected override async _afterCreate(
command: MongoEntityService.CreateCommand<OrderLine>,
result: PartialDTO<OrderLine>,
) {
await this.inventory.for(this.context).updateOnly(result.productId, {
$inc: { stock: -result.quantity },
} as any);
}
}
Input validation
Run custom validation that goes beyond schema constraints — for example, checking uniqueness:
protected override async _beforeCreate(command: MongoEntityService.CreateCommand<Customer>) {
const exists = await this.existsOne({ filter: { email: command.input.email } });
if (exists) throw new ConflictError(`Email ${command.input.email} is already registered`);
}
Audit logging
Record who changed what and when in a separate audit collection:
protected override async _afterCreate(
command: MongoEntityService.CreateCommand<Customer>,
result: PartialDTO<Customer>,
) {
await this.auditLog.insertOne({
action: 'create',
resourceId: result._id,
userId: this.context?.userId,
at: new Date(),
});
}
protected override async _afterDelete(
command: MongoEntityService.DeleteCommand<Customer>,
affected: number,
) {
if (affected) {
await this.auditLog.insertOne({
action: 'delete',
resourceId: command.documentId,
userId: this.context?.userId,
at: new Date(),
});
}
}
Cache invalidation
Bust a cache whenever a document is mutated:
protected override async _afterUpdate(
command: MongoEntityService.UpdateOneCommand<Customer>,
result: PartialDTO<Customer> | undefined,
) {
if (result) {
await this.cache.del(`customer:${command.documentId}`);
}
}