Hooks & Security
While the API Generator handles all standard CRUD operations automatically, enterprise applications inevitably require custom business logic. You might need to hash a password before it saves to the database, or send a welcome email immediately after a user registers.
JSONExpress handles this cleanly using Schema Lifecycle Hooks.
The Lifecycle
Hooks are functions you define directly inside your defineModel configuration. The Database Adapter guarantees they will execute at precisely the right moment.
There are four available hooks:
beforeCreate(payload, context)afterCreate(record, context)beforeUpdate(patch, context)afterUpdate(record, patch, context)
Mutating Payloads (before hooks)
The beforeCreate and beforeUpdate hooks receive the incoming payload before it touches the database. You can inspect the payload, modify it, and return the modified version. If you return a value, the adapter will use your returned object for the database operation.
Example: Argon2 Password Hashing
This is how the @json-express/plugin-identity secures your passwords. It intercepts the raw payload, hashes the password, and strips the plain-text string.
import { defineModel, types } from '@json-express/core';
import * as argon2 from 'argon2';
export default defineModel({
name: 'users',
fields: {
id: types.id(),
email: types.string(),
passwordHash: types.string()
},
hooks: {
beforeCreate: async (payload, ctx) => {
if (payload.password) {
// Hash the password securely
payload.passwordHash = await argon2.hash(payload.password);
// CRITICAL: Strip the plain-text password so it never hits the DB
delete payload.password;
}
return payload; // Return the mutated payload
}
}
});Aborting Operations
If a payload violates a strict security requirement, you can simply throw new Error() inside a before hook. The Database Adapter will instantly abort the operation and the REST API will return a 500 (or 400 if you throw a specific known error type).
Triggering Side-Effects (after hooks)
The afterCreate and afterUpdate hooks are executed after the database has successfully committed the data. The record argument contains the final, committed object (including the auto-generated id).
The Hook Context (ctx)
Every hook receives a HookContext object. This is your gateway to the rest of the framework. It contains:
ctx.db: The activeIDatabaseAdapter(allowing you to fetch related records).ctx.logger: The system logger.ctx.kvStore: Access to the ephemeral Key-Value store.ctx.queue: Access to the distributed task queue.
Example: Sending Asynchronous Emails
Sending an email is a slow, blocking operation. You should never await an email provider directly inside a hook, because it will force the end-user to wait for the HTTP response.
Instead, use ctx.queue to instantly offload the task to a background worker:
export default defineModel({
name: 'users',
fields: { /* ... */ },
hooks: {
afterCreate: async (record, ctx) => {
// Log the creation
ctx.logger.info(`New user registered: ${record.email}`);
// Enqueue a welcome email to the background worker
// This returns instantly, allowing the HTTP response to finish!
if (ctx.queue) {
await ctx.queue.enqueue('emails', 'sendWelcomeEmail', {
userId: record.id,
email: record.email
});
}
}
}
});Security Best Practices
- Never trust the client: Use
beforehooks to forcefully sanitize incoming data if the standard Field-Level access controls (access: { create: 'admin' }) are not granular enough. - Never block the event loop: Always offload CPU-intensive operations (like PDF generation) or slow network calls (like webhooks and emails) to
ctx.queueduring anafterhook.