Skip to content

Hooks

Data has a lifecycle. A record is created, updated, submitted, deleted. At each of these moments you might need something to happen. Validate the data before it is saved. Enrich it with related information. Trigger a side effect after it persists. Hooks give you precise insertion points into this lifecycle.

They are small by design. A hook answers one question: "when this event happens to this model, what should run?"

Defining hooks

typescript
// modules/sales/hooks/order.ts
import { defineHooks } from 'rangka';

export default defineHooks('sales.order', {
  validate(doc) {
    if (!doc.items || doc.items.length === 0) {
      throw new Error('Sales Order must have at least one item');
    }
  },

  async beforeSave(doc, ctx) {
    doc.customer_name = await getCustomerName(doc.customer, ctx);
  },

  async afterCreate(doc, ctx) {
    await ctx.enqueue('sales.reserve-stock', { orderId: doc.id });
    await ctx.enqueue('sales.send-confirmation', { orderId: doc.id });
  },
});

Available hooks

HookWhenCan modify docCan abort
validateBefore any writeNoYes (throw)
beforeSaveBefore create or updateYesYes (throw)
beforeCreateBefore insertYesYes (throw)
beforeUpdateBefore updateYesYes (throw)
beforeDeleteBefore deleteNoYes (throw)
afterSaveAfter create or update (post-commit)NoNo
afterCreateAfter insertNoNo
afterUpdateAfter updateNoNo
afterDeleteAfter deleteNoNo

Execution order

1. validate
2. beforeSave
3. beforeCreate (or beforeUpdate)
4. --- database write ---
5. afterCreate (or afterUpdate)
6. --- transaction commits ---
7. afterSave (outside transaction, cannot roll back the write)

The context object

The ctx parameter gives you access to the framework:

typescript
interface FrameworkContext {
  db: DatabaseClient;
  schema: SchemaRegistry;
  scope: unknown;
  auth: { user: Record<string, unknown> | null; roles: string[] };
  config: Record<string, unknown>;
  events: { emit(event, payload): Promise<void> };
  service(name: string): ServiceInstance;
  enqueue(job, data, opts?): Promise<void>;
  notify(channel, message): void;
  email: { send(template, options): Promise<void> };
}

Common patterns

Validation

typescript
defineHooks('sales.invoice', {
  validate(doc) {
    if (doc.posting_date > new Date()) {
      throw new Error('Posting date cannot be in the future');
    }
    for (const item of doc.items || []) {
      if (item.qty <= 0) {
        throw new Error(`Item ${item.item_name}: quantity must be positive`);
      }
    }
  },
});

Throwing an error aborts the operation and returns a 400 to the client.

Data enrichment

typescript
defineHooks('sales.order', {
  async beforeSave(doc, ctx) {
    if (doc.customer) {
      const customer = await ctx.db
        .selectFrom('sales.customer')
        .where('id', '=', doc.customer)
        .selectAll()
        .executeTakeFirst();

      doc.customer_name = customer?.customer_name;
      doc.currency = customer?.default_currency || 'USD';
    }
  },
});

Side effects

typescript
defineHooks('inventory.stock_entry', {
  async afterCreate(doc, ctx) {
    await ctx.service('inventory.stock').updateStockLedger(doc);
    await ctx.enqueue('inventory.reorder-check', { warehouse: doc.warehouse });
  },
});

Preventing deletion

typescript
defineHooks('sales.customer', {
  async beforeDelete(doc, ctx) {
    const orders = await ctx.db
      .selectFrom('sales.order')
      .where('customer', '=', doc.id)
      .select('id')
      .limit(1)
      .execute();

    if (orders.length > 0) {
      throw new Error('Cannot delete customer with existing orders');
    }
  },
});

Multiple hook sources

A model can have hooks from its own module, from extensions, and from traits. They all chain together and execute in registration order. A validate hook that throws stops the entire pipeline.

When to use hooks vs services vs computed fields

  • Hooks for side effects and validation tied to data events. They can call services when the logic is complex.
  • Computed fields for deriving values from other fields purely through calculation.
  • Services for reusable logic called from multiple places (actions, other services, jobs).