Skip to content

Lifecycle Hooks

Hooks allow you to execute logic before insertions, updates and after loading entities. They are methods of the class itself decorated with one of the three available hooks.


DecoratorWhen it fires
@BeforeInsert()Before INSERT — after dirty check, before timestamps
@BeforeUpdate()Before UPDATE — after dirty check, before @UpdatedAt
@AfterLoad()After any SELECT that hydrates the entity

Decorate an entity method with the desired hook. The method takes no arguments — use this to access the instance:

import { Entity, PrimaryColumn, Column, BeforeInsert, BeforeUpdate, AfterLoad } from 'mirror-orm';
@Entity('users')
class User {
@PrimaryColumn({ strategy: 'uuid_v7' })
id!: string;
@Column()
email!: string;
@Column({ select: false })
passwordHash!: string;
@Column()
emailNormalized!: string;
@BeforeInsert()
normalizeEmail() {
this.emailNormalized = this.email.toLowerCase().trim();
}
@BeforeUpdate()
onUpdate() {
this.emailNormalized = this.email.toLowerCase().trim();
}
@AfterLoad()
onLoad() {
// Chamado toda vez que esta entidade for carregada do banco
console.log(`User ${this.id} carregado`);
}
}

Runs before every INSERT. Useful for normalization, generation of derived fields or business validation.

Execution order in INSERT:

1. @BeforeInsert() roda
2. @CreatedAt → new Date()
3. @UpdatedAt → new Date()
4. INSERT executado

The hook cannot prevent the assignment of timestamps — they are always set after the hook. If you change timestamp fields in the hook, they will be overwritten.

@BeforeInsert()
prepareSlug() {
this.slug = this.title
.toLowerCase()
.replace(/\s+/g, '-')
.replace(/[^\w-]/g, '');
}

Runs before every UPDATE. Called only when save() detects columns changed by dirty check — if nothing has changed, the hook does not run.

Execution order in UPDATE:

1. @BeforeUpdate() roda
2. @UpdatedAt → new Date()
3. UPDATE executado (apenas colunas alteradas)
@BeforeUpdate()
incrementRevision() {
this.revision = (this.revision ?? 0) + 1;
}

Runs after any operation that hydrates the entity: find(), findOne(), findById(), findAll(), and also when loading relations.

@AfterLoad()
decryptSensitiveField() {
if (this.encryptedData) {
this.data = decrypt(this.encryptedData);
}
}

@AfterLoad does not run in bulk operations such as update(data, where), delete(where) or upsert() — only in operations that return hydrated entities.


All hooks support async. Mirror waits for each hook before proceeding:

@BeforeInsert()
async validateWithExternalService() {
const isValid = await externalValidator.check(this.email);
if (!isValid) {
throw new Error(`Email inválido: ${this.email}`);
}
}

Hooks run in series — if an entity has multiple hooks of the same type, each one is waited before the next one starts.

Throwing an exception within a hook cancels the operation and, if there is an active transaction, triggers the rollback automatically:

@BeforeInsert()
async checkUniqueness() {
const exists = await conn.getRepository(User).exists({ email: this.email });
if (exists) {
throw new Error('Email já cadastrado'); // → operação cancelada
}
}

Hooks run within the active transaction context via AsyncLocalStorage. Queries made within a hook participate in the same transaction:

@BeforeInsert()
async createAuditEntry() {
// Esta query participa da mesma transação que o INSERT da entidade
await conn.getRepository(AuditLog).save(
Object.assign(new AuditLog(), {
action: 'user.created',
targetId: this.id,
})
);
}

Children inherit their father’s hooks. If the child declares the same hook type, it overrides the parent’s — there is no automatic composition:

@Entity({ tableName: 'notifications', discriminatorColumn: 'type' })
class Notification {
@BeforeInsert()
setDefaults() {
this.readAt = null;
}
}
@ChildEntity('email')
class EmailNotification extends Notification {
@Column()
toAddress!: string;
@BeforeInsert()
validate() {
// O setDefaults() do pai NÃO roda — este hook substitui
if (!this.toAddress) throw new Error('toAddress obrigatório');
}
}
@ChildEntity('push')
class PushNotification extends Notification {
// Sem @BeforeInsert declarado → herda setDefaults() do pai
}

If you need the parent’s behavior in the child, explicitly call:

@BeforeInsert()
validate() {
super.setDefaults(); // chama o hook do pai manualmente
if (!this.toAddress) throw new Error('toAddress obrigatório');
}

  • @AfterInsert, @AfterUpdate, @BeforeRemove, @AfterRemove — do not exist
  • Receive arguments in the hook method
  • Cancel operation via return value — throw an exception
  • Run in bulk operations (update(data, where), delete(where), upsert())

See also: @ChildEntity hook inheritance is covered in Inheritance & Composition. Hooks that run queries automatically participate in active transactions — see Transactions.