Hooks & lifecycle events
Read this page in the documentation
Hooks & lifecycle events Hooks are functions that Prorm runs automatically at well-defined points in a model's lifecycle — before and after validation, creation, updates, deletes, and finds. They let you hash a password before it is written, stamp an audit field, cascade side effects, or veto an operation by throwing — all without scattering that logic across every call site. Every hook is dispatched through the model's HooksManager, which stores the handlers you register and invokes them in registration order. This guide covers the complete set of lifecycle events, how to attach handlers, what arguments they receive, the order in which they fire, and some practical patterns. Firing order beforeSave and afterSave wrap both create and update, so logic that applies to either belongs there rather than duplicated across the pair. Inside a transaction the hooks run on the same transaction as the statement, so a throw rolls back the write along with anything the hook did. The lifecycle hooks that exist Prorm defines these hooks (the HookName union / ModelHooks interface). They come in before / after pairs: Hook | Fires around | --- | --- | beforeValidate / afterValidate | instance validation | beforeCreate / afterCreate | inserting a single record | beforeUpdate / afterUpdate | updating a single record | beforeDestroy / afterDestroy | deleting a single record | beforeSave / afterSave | instance.save() (covers both insert and update) | beforeBulkCreate / afterBulkCreate | Model.bulkCreate() | beforeBulkUpdate / afterBulkUpdate | Model.update() (static, multi-row) | beforeBulkDestroy / afterBulkDestroy | Model.destroy() (static, multi-row) | beforeFind / afterFind | findAll / findOne queries | Two more pairs are dispatched by their operations and can be registered the same way: beforeReload / afterReload (around instance.reload()) and beforeUpsert / afterUpsert (around Model.upsert()). Defining hooks in define() The most common way to attach hooks is the hooks object in the third argument of define(). Each key is a hook name and each value is a handler. Handlers may be synchronous or async; Prorm always awaits them, so returning a promise is fully supported. Attaching hooks after definition You can also register hooks on an already-defined model. hook() (and its alias addHook()) append a handler; there are matching named convenience methods. Registration is additive — calling hook() twice for the same event registers two handlers, and both run. To inspect or remove handlers: Hook arguments Most hooks are instance hooks and receive (instance, options): instance — the model instance being created, updated, or destroyed. Mutating its attributes in a before hook changes what gets written to the database. options — the options passed to the triggering call (a HookOptions), carrying fields such as transaction, hooks, validate, fields, and model (the model class). Use options.transaction to keep any additional queries you run inside the same transaction. Bulk hooks (beforeBulk / afterBulk) receive (instances, options), where instances is the array of affected records: Find hooks are shaped differently. beforeFind receives (findOptions, model) and can mutate the query before it runs; afterFind receives (instances, findOptions, model) and sees the results: Ordering Hooks fire in a fixed sequence per operation. Within a single event, multiple registered handlers run in the order they were added. Model.create(values) runs the create hooks around validation: instance.save() for a new record layers the validate and save hooks around the underlying insert: For an existing, changed instance, save() follows the same beforeValidate → beforeSave → UPDATE → afterSave shape. instance.update(values) runs: instance.destroy() runs beforeDestroy → DELETE → afterDestroy. For a paranoid model the delete is a soft-delete (setting deletedAt), but the same two hooks still bracket it. findAll / findOne run beforeFind → (query) → afterFind. Bulk operations wrap the whole batch in the bulk pair. When you pass individualHooks: true, the matching per-row hook also fires for each affected instance: Note that save() performs its insert/update internally with hooks disabled, so it does not additionally fire beforeCreate / afterCreate or beforeUpdate / afterUpdate — use beforeSave / afterSave to cover both paths, or call Model.create() directly when you specifically want the create hooks. Skipping hooks Every write accepts hooks: false to bypass hook execution entirely — useful for seeds, migrations, or internal maintenance where you do not want side effects to fire. By default hooks are on (hooks defaults to true); passing hooks: false is the only value that disables them. Validation hooks beforeValidate / afterValidate fire around instance.validate() only when validation hooks are requested — the save path requests them, and you can also opt in explicitly: beforeValidate is the right place to coerce or normalise input so that the values that reach the validators are already clean: Practical patterns Hashing a password before write. A beforeCreate hook keeps hashing logic in one place: Vetoing an operation. Throwing from any before hook aborts the operation — the error propagates to the caller and the write never happens. The HooksManager wraps thrown errors as Error in <Model>.<hook> hook: <message> so failures are easy to trace. Cascading side effects transactionally. Use options.transaction so the side effect commits or rolls back with the triggering write: Cache invalidation on delete. afterDestroy runs once the row is gone: Because hooks are just functions registered on the model, you can compose them freely — layer several handlers on one event, register cross-cutting concerns (audit, cache, search indexing) from separate modules, and disable them per-call with hooks: false whenever you need the raw operation. Next: Transactions — making several writes succeed or fail together. Related reading Core concepts — the reading path these belong to Going further — the specialised material