Hooks System
The hooks system provides data transformation and side effects during database operations.
Overview
Hooks allow you to:
- Transform data before it's saved to the database
- Transform data before it's returned to the user
- Perform validation beyond basic field rules
- Trigger side effects (logging, notifications, etc.)
Hook Types
List-Level Hooks
Defined at the list level, these hooks run for all operations on the list:
typescript
Post: list({
fields: {
/* ... */
},
hooks: {
resolveInput: async ({ resolvedData, operation, context }) => {
// Transform input data before database operation
if (operation === 'create' && resolvedData.status === 'published') {
resolvedData.publishedAt = new Date()
}
return resolvedData
},
validateInput: async ({ resolvedData, addValidationError }) => {
// Custom validation logic
if (resolvedData.title?.includes('spam')) {
addValidationError('Title cannot contain spam')
}
},
beforeOperation: async ({ operation, resolvedData, context }) => {
// Side effects before database operation
console.log(`About to ${operation} a post`)
},
afterOperation: async ({ operation, item, context }) => {
// Side effects after database operation
if (operation === 'create') {
// Send notification, invalidate cache, etc.
}
},
},
})
Field-Level Hooks
Defined on individual fields:
typescript
fields: {
password: password({
hooks: {
resolveInput: async ({ resolvedData, fieldKey }) => {
// Hash password before saving
const plaintext = resolvedData[fieldKey]
if (plaintext) {
return await bcrypt.hash(plaintext, 10)
}
},
resolveOutput: async ({ item, fieldKey }) => {
// Wrap with HashedPassword class
return new HashedPassword(item[fieldKey])
},
},
}),
}
Hook Execution Order
Write Operations (create/update)
- List-level
resolveInput- Transform input data at list level - Field-level
resolveInput- Transform individual field values - List-level
validateInput- Custom validation logic - Field validation - Built-in rules (isRequired, length, min/max)
- Field-level access control - Filter writable fields
- Field-level
beforeOperation- Side effects for individual fields - List-level
beforeOperation- Side effects at list level - Database operation
- List-level
afterOperation- Side effects at list level - Field-level
afterOperation- Side effects for individual fields
Read Operations (query)
- Database operation
- Field-level access control - Filter readable fields
- Field-level
resolveOutput- Transform individual field values - Field-level
afterOperation- Side effects for individual fields
Hook Context
All hooks receive a context object with relevant information:
typescript
interface HookContext {
operation: 'create' | 'update' | 'delete' | 'query'
session: Session | null
context: Context
listKey: string
resolvedData?: any // For input hooks
item?: any // For output hooks
originalInput?: any // Original input before transformations
}
Common Use Cases
Auto-Set Timestamps
typescript
resolveInput: async ({ resolvedData, operation }) => {
if (operation === 'create') {
resolvedData.createdAt = new Date()
}
if (operation === 'update') {
resolvedData.updatedAt = new Date()
}
return resolvedData
}
Slug Generation
typescript
fields: {
slug: text({
hooks: {
resolveInput: async ({ resolvedData, item, operation }) => {
// Generate slug from title if not provided
if (!resolvedData.slug && resolvedData.title) {
return resolvedData.title
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '')
}
},
},
}),
}
Password Hashing
typescript
password: password({
hooks: {
resolveInput: async ({ resolvedData, fieldKey }) => {
const plaintext = resolvedData[fieldKey]
if (plaintext) {
return await bcrypt.hash(plaintext, 10)
}
},
},
})
Cache Invalidation
typescript
afterOperation: async ({ operation, item, context }) => {
if (['create', 'update', 'delete'].includes(operation)) {
// Invalidate cache
await redis.del(`post:${item.id}`)
}
}
Audit Logging
typescript
beforeOperation: async ({ operation, resolvedData, context }) => {
await context.db.auditLog.create({
data: {
operation,
userId: context.session?.userId,
timestamp: new Date(),
data: resolvedData,
},
})
}
Best Practices
1. Keep Hooks Pure
Avoid side effects in resolveInput and resolveOutput. Use beforeOperation and afterOperation for side effects:
typescript
// ✅ Good: Pure transformation
resolveInput: ({ resolvedData }) => {
resolvedData.title = resolvedData.title.trim()
return resolvedData
}
// ❌ Bad: Side effects in resolveInput
resolveInput: async ({ resolvedData, context }) => {
await sendEmail() // Don't do this here!
return resolvedData
}
2. Use Async When Needed
All hooks can be async:
typescript
resolveInput: async ({ resolvedData }) => {
const result = await someAsyncOperation()
resolvedData.field = result
return resolvedData
}
3. Return Modified Data
Always return the modified data from resolveInput:
typescript
// ✅ Good: Returns modified data
resolveInput: ({ resolvedData }) => {
resolvedData.slug = generateSlug(resolvedData.title)
return resolvedData
}
// ❌ Bad: Doesn't return
resolveInput: ({ resolvedData }) => {
resolvedData.slug = generateSlug(resolvedData.title)
// Missing return!
}
Next Steps
- Access Control - Secure your data
- Field Types - Available field types
- Custom Fields - Create custom field types