Hooks
Client lifecycle hooks for auditing, tracing, metrics, and request-scoped logging.
Client hooks are the side-effect layer around Better operations. They are optional - if you do not need them, do not pass them. They are ideal for cross-cutting concerns you do not want duplicated in every call:
- audit trails
- tracing and metrics
- authorization checks
- request-scoped logging
You register hooks when creating the client:
const client = better(db, {
schema,
hooks: {
beforeCreate(ctx) {
console.log('beforeCreate', ctx.action, ctx.table);
},
afterCreate(ctx) {
console.log('afterCreate', ctx.row);
},
beforeQuery(ctx) {
console.log('beforeQuery', ctx.action, ctx.args.where);
},
afterQuery(ctx) {
console.log('afterQuery', ctx.action, ctx.result);
},
},
});Available hooks
| Hook | action values |
|---|---|
beforeCreate / afterCreate | create, createMany, upsert, upsertMany |
beforeUpdate / afterUpdate | update, updateMany, updateEach |
beforeDelete / afterDelete | delete, deleteMany |
beforeQuery / afterQuery | findMany, findFirst, findOne, findUnique, count, exists, paginate, cursor |
upsert fires the create hooks only, with action: 'upsert'.
beforeTransactionafterTransactionCommitafterTransactionRollbackonTransactionError
beforeRawafterRawonRawError
onError- fires when an operation or a CRUD/query hook throws.
Hook return values are ignored. A hook that throws fails the call (a throwing before hook also prevents the database call). onError receives the error with code HOOK_ERROR; the caller gets a BetterDrizzleError with code OPERATION_ERROR that keeps the original message, hookName, and stage. $withoutPlugins() skips plugins, not client hooks.
Hook context
CRUD and query hooks
Prop
Type
Batch writes (createMany, updateMany, updateEach, deleteMany, upsertMany) have result only, a { count, data? } summary.
onError
onError receives the same fields as the CRUD context (without result, row, and rows), plus:
Prop
Type
It also fires when .throw() finds no row (RESULT_NOT_FOUND, stage operation). Errors thrown inside onError are swallowed.
Transaction hooks
All four receive client (the transaction client), name, comment, depth (1 for the outer transaction, higher for savepoints), attempt (starts at 1), meta, transactionOptions, transactionContext, isInTransaction: true, db, schema, options, afterCommit, and afterRollback.
afterTransactionRollbackaddsreason: therollback(...)reason, or the error.onTransactionErroraddserror. It fires beforeafterTransactionRollback, and not for an explicitrollback().
Raw hooks
beforeRaw, afterRaw, and onRawError receive action ('raw' | 'executeRaw' | 'rawUnsafe'), query (rendered SQL text), sql, rawOptions, name, comment, timeoutMs, signal, map, meta, isInTransaction, transaction, transactionContext, db, schema, options, afterCommit, and afterRollback. afterRaw adds result, and onRawError adds error.
Request metadata with meta
Every operation accepts a meta object. Use it to thread request-scoped context into hooks:
await client.users.findMany({
where: { active: true },
meta: { requestId: 'req_123', userId: 'admin_7' },
});const client = better(db, {
schema,
hooks: {
beforeQuery(ctx) {
console.log(ctx.meta?.requestId);
},
},
});Transaction lifecycle
const client = better(db, {
schema,
hooks: {
beforeTransaction(ctx) {
console.log('tx start', ctx.name, ctx.depth);
},
afterTransactionCommit(ctx) {
console.log('tx committed', ctx.attempt);
},
afterTransactionRollback(ctx) {
console.log('tx rolled back', ctx.reason);
},
onTransactionError(ctx) {
console.error('tx error', ctx.error);
},
},
});Raw SQL lifecycle
const client = better(db, {
schema,
hooks: {
beforeRaw(ctx) {
console.log(ctx.action, ctx.comment, ctx.name);
},
afterRaw(ctx) {
console.log(ctx.result);
},
onRawError(ctx) {
console.error(ctx.query, ctx.error);
},
},
});Hooks vs plugins
Observe with hooks, change behavior with plugins
Client hooks should observe and coordinate - logging, tracing, metrics.
If you want to mutate an operation (rewrite the where, inject fields, add
delegate methods, expose typed operation args), that is
plugin territory.