Lifecycle Hooks
Lifecycle Hooks
Lifecycle hooks allow you to execute custom logic before and after CRUD operations. Perfect for audit logging, notifications, data transformation, and business logic.
Available Hooks
| Hook | When it runs | Can modify data | Use cases |
|---|---|---|---|
beforeCreate | Before inserting record | ✅ Yes | Set defaults, validate, transform |
afterCreate | After inserting record | ❌ No | Send notifications, log audit |
beforeUpdate | Before updating record | ✅ Yes | Validate changes, transform |
afterUpdate | After updating record | ❌ No | Notify users, sync services |
beforeDelete | Before deleting record | ❌ No | Check dependencies, validate |
afterDelete | After deleting record | ❌ No | Clean up relations, log audit |
beforeList | Before listing records | ❌ No | Log access (rarely needed) |
afterList | After listing records | ❌ No | Log access, track analytics |
beforeGet | Before fetching single record | ❌ No | Log access (rarely needed) |
afterGet | After fetching single record | ❌ No | Track views, log access |
Hook Configuration Methods
Nuxt Auto API supports three ways to configure hooks, with clear priority ordering.
Method 1: Per-Resource (via Module Registration)
Highest priority. Best for module-based architecture.
// playground/modules/base/index.ts
import { createModuleImport } from '@websideproject/nuxt-auto-api'
export default defineNuxtModule({
async setup(options, nuxt) {
const resolver = createResolver(import.meta.url)
nuxt.hook('autoApi:registerSchema', (registry) => {
registry.register('users', {
schema: createModuleImport(resolver.resolve('./schema'), 'users'),
hooks: createModuleImport(resolver.resolve('./hooks'), 'userHooks')
})
})
}
})
// playground/modules/base/hooks.ts
export const userHooks = {
beforeCreate: async (data, context) => {
data.createdBy = context.user.id
data.status = data.status || 'active'
return data
},
afterCreate: async (user, context) => {
console.log(`User ${user.id} created by ${context.user.id}`)
// Send welcome email, create audit log, etc.
}
}
Method 2: Plugin-Based (Runtime Registration)
Medium priority. Best for cross-cutting concerns.
// server/plugins/user-hooks.ts
export default defineNitroPlugin(() => {
const hooks = globalThis.__autoApiHooks || (globalThis.__autoApiHooks = {})
hooks.users = {
...(hooks.users || {}),
afterCreate: async (user, context) => {
// Send welcome email
await sendEmail({
to: user.email,
template: 'welcome',
data: { name: user.name }
})
},
afterUpdate: async (user, context) => {
// Invalidate cache
await clearCache(`user:${user.id}`)
}
}
})
Method 3: Config-Based (nuxt.config.ts)
Lowest priority. Best for simple projects.
// nuxt.config.ts
export default defineNuxtConfig({
autoApi: {
hooks: {
users: {
beforeCreate: async (data, context) => {
data.createdBy = context.user.id
return data
},
afterCreate: async (user, context) => {
console.log('User created:', user.id)
}
},
posts: {
beforeCreate: async (data, context) => {
data.authorId = context.user.id
data.publishedAt = data.publishedAt || new Date()
return data
}
}
}
}
})
Hook Priority & Merging
When multiple hooks exist for the same event:
- Registry hooks (createModuleImport) - execute last (highest priority)
- Plugin hooks (globalThis) - execute second
- Config hooks (nuxt.config.ts) - execute first (lowest priority)
// If all three exist:
// 1. Config hook runs
// 2. Plugin hook runs
// 3. Registry hook runs
// 4. Final result from registry hook is used
Before Hooks (Data Modification)
Before hooks can modify data before it's saved:
export const productHooks = {
beforeCreate: async (data, context) => {
// Set defaults
data.status = data.status || 'draft'
data.createdBy = context.user.id
// Generate slug from title
data.slug = slugify(data.title)
// Validate business rules
if (data.price < 0) {
throw new Error('Price cannot be negative')
}
// Transform data
data.name = data.name.trim().toLowerCase()
// Return modified data
return data
},
beforeUpdate: async (id, data, context) => {
// Track who modified
data.modifiedBy = context.user.id
data.modifiedAt = new Date()
// Prevent changing certain fields
delete data.createdBy
delete data.createdAt
return data
}
}
After Hooks (Side Effects)
After hooks are for side effects (no data modification):
export const orderHooks = {
afterCreate: async (order, context) => {
// Send confirmation email
await sendEmail({
to: order.customerEmail,
template: 'order-confirmation',
data: { orderNumber: order.id, total: order.total }
})
// Create audit log
await db.insert(auditLogs).values({
action: 'order.created',
userId: context.user.id,
resourceId: order.id,
timestamp: new Date()
})
// Notify external service
await fetch('https://analytics.example.com/track', {
method: 'POST',
body: JSON.stringify({
event: 'order_created',
orderId: order.id,
total: order.total
})
})
},
afterUpdate: async (order, context) => {
// If status changed to 'shipped'
if (order.status === 'shipped') {
await sendEmail({
to: order.customerEmail,
template: 'order-shipped',
data: { trackingNumber: order.trackingNumber }
})
}
},
afterDelete: async (id, context) => {
// Clean up related records
await db.delete(orderItems).where(eq(orderItems.orderId, id))
// Log deletion
console.log(`Order ${id} deleted by user ${context.user.id}`)
}
}
Hook Context
All hooks receive a HandlerContext object:
interface HandlerContext {
db: any // Database instance
schema: any // Drizzle schema
user: AuthUser | null // Current user
permissions: string[] // User permissions
params: Record<string, string> // Route params
query: Record<string, any> // Query parameters
validated: { // Validated data
body?: any
query?: any
}
event: H3Event // H3 event object
resource: string // Resource name (e.g., 'users')
operation: string // Operation type
tenant?: { // Multi-tenancy info
id: string | number
field: string
canAccessAllTenants: boolean
}
resourceConfig?: ResourceRegistration // Resource configuration
}
Using context:
export const postHooks = {
beforeCreate: async (data, context) => {
// Auto-set author from authenticated user
data.authorId = context.user.id
// Auto-set tenant
if (context.tenant) {
data.organizationId = context.tenant.id
}
// Access query parameters
if (context.query.publishNow === 'true') {
data.publishedAt = new Date()
}
return data
},
afterCreate: async (post, context) => {
// Log to database
await context.db.insert(auditLogs).values({
action: 'post.created',
userId: context.user.id,
postId: post.id
})
}
}
Error Handling
Before Hooks
Before hooks should throw errors to block the operation:
beforeCreate: async (data, context) => {
// Validation error - blocks creation
if (!data.email || !data.email.includes('@')) {
throw new Error('Invalid email address')
}
// Business rule error - blocks creation
const existingUser = await context.db.query.users.findFirst({
where: eq(users.email, data.email)
})
if (existingUser) {
throw new Error('Email already exists')
}
return data
}
After Hooks
After hooks should not throw errors (or set errorHandling: 'throw'):
// Default behavior: log errors, don't rollback
afterCreate: async (user, context) => {
try {
await sendWelcomeEmail(user.email)
} catch (error) {
// Error is logged but operation continues
console.error('Failed to send welcome email:', error)
}
}
Configure error handling:
export default defineNuxtConfig({
autoApi: {
hookConfig: {
// 'log' (default): log errors but don't throw
// 'throw': throw errors and rollback
errorHandling: 'log',
// Hook timeout in milliseconds
timeout: 5000,
// Execute multiple hooks in parallel
parallel: false
}
}
})
Hook Execution in Bulk Operations
Hooks execute for each item in bulk operations:
// Bulk create 100 users
POST /api/users/bulk { items: [/* 100 users */] }
// beforeCreate runs 100 times (once per user)
// afterCreate runs 100 times (once per user)
Common Use Cases
Audit Logging
const auditHooks = {
afterCreate: async (record, context) => {
await db.insert(auditLogs).values({
action: `${context.resource}.created`,
userId: context.user.id,
resourceId: record.id,
changes: JSON.stringify(record)
})
},
afterUpdate: async (record, context) => {
await db.insert(auditLogs).values({
action: `${context.resource}.updated`,
userId: context.user.id,
resourceId: record.id,
changes: JSON.stringify(record)
})
}
}
Notifications
const notificationHooks = {
afterCreate: async (comment, context) => {
// Notify post author about new comment
const post = await db.query.posts.findFirst({
where: eq(posts.id, comment.postId)
})
if (post && post.authorId !== context.user.id) {
await sendNotification(post.authorId, {
type: 'new_comment',
message: `${context.user.name} commented on your post`
})
}
}
}
Data Validation
const validationHooks = {
beforeCreate: async (data, context) => {
// Complex business rule validation
if (data.price > 1000 && !context.permissions.includes('admin')) {
throw new Error('Only admins can create items over $1000')
}
// Cross-field validation
if (data.discountPercent > 0 && !data.discountCode) {
throw new Error('Discount code required when discount is applied')
}
return data
}
}
Cache Invalidation
const cacheHooks = {
afterUpdate: async (record, context) => {
await clearCache(`${context.resource}:${record.id}`)
await clearCache(`${context.resource}:list`)
},
afterDelete: async (id, context) => {
await clearCache(`${context.resource}:${id}`)
await clearCache(`${context.resource}:list`)
}
}
External Service Integration
const integrationHooks = {
afterCreate: async (customer, context) => {
// Sync to CRM
await fetch('https://crm.example.com/api/customers', {
method: 'POST',
body: JSON.stringify(customer)
})
// Add to mailing list
await mailchimp.lists.addMember({
email: customer.email,
firstName: customer.firstName,
lastName: customer.lastName
})
}
}
Best Practices
- Keep hooks focused: One responsibility per hook
- Handle errors: Use try-catch in after hooks
- Don't query in before hooks: Data should already be validated
- Use after hooks for async operations: Email, webhooks, etc.
- Return modified data from before hooks: Always return the data object
- Don't modify data in after hooks: Too late, data is already saved
- Log important operations: Especially in production
- Test hooks thoroughly: They can break your API if buggy
- Use transactions carefully: After hooks run outside transaction scope
- Monitor hook performance: Set appropriate timeouts
Migration Path
If you have custom handlers, migrate to hooks:
// Before: Custom handler
export default defineEventHandler(async (event) => {
const body = await readBody(event)
// Custom logic
body.createdBy = event.context.user.id
body.status = 'active'
const user = await db.insert(users).values(body).returning()
// Send email
await sendWelcomeEmail(user.email)
return { data: user }
})
// After: Use hooks
export const userHooks = {
beforeCreate: async (data, context) => {
data.createdBy = context.user.id
data.status = 'active'
return data
},
afterCreate: async (user, context) => {
await sendWelcomeEmail(user.email)
}
}
Benefits:
- Automatic validation
- Authorization checks
- Multi-tenancy support
- Consistent error handling
- Works with bulk operations
Aggregations
Nuxt Auto API provides powerful aggregation capabilities for analyzing your data, including simple aggregates on list endpoints and complex grouped aggregations.
Many-to-Many (M2M) Relationships
This guide covers how to work with many-to-many relationships in @websideproject/nuxt-auto-api.