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.

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

HookWhen it runsCan modify dataUse cases
beforeCreateBefore inserting record✅ YesSet defaults, validate, transform
afterCreateAfter inserting record❌ NoSend notifications, log audit
beforeUpdateBefore updating record✅ YesValidate changes, transform
afterUpdateAfter updating record❌ NoNotify users, sync services
beforeDeleteBefore deleting record❌ NoCheck dependencies, validate
afterDeleteAfter deleting record❌ NoClean up relations, log audit
beforeListBefore listing records❌ NoLog access (rarely needed)
afterListAfter listing records❌ NoLog access, track analytics
beforeGetBefore fetching single record❌ NoLog access (rarely needed)
afterGetAfter fetching single record❌ NoTrack 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:

  1. Registry hooks (createModuleImport) - execute last (highest priority)
  2. Plugin hooks (globalThis) - execute second
  3. 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

  1. Keep hooks focused: One responsibility per hook
  2. Handle errors: Use try-catch in after hooks
  3. Don't query in before hooks: Data should already be validated
  4. Use after hooks for async operations: Email, webhooks, etc.
  5. Return modified data from before hooks: Always return the data object
  6. Don't modify data in after hooks: Too late, data is already saved
  7. Log important operations: Especially in production
  8. Test hooks thoroughly: They can break your API if buggy
  9. Use transactions carefully: After hooks run outside transaction scope
  10. 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

Need a Landing Page?

Modern landing pages with optional modules (blog, docs, forms, i18n). Let's discuss your project.

Build Your MVP

Full-stack SaaS development. Expert in database design, multi-tenancy, and scalable architecture.

Deployment Help

Dockerize your backend, set up CI/CD pipelines, deploy to Cloudflare or Hetzner. Early-stage setup.

Suggest a SaaS Tool

Missing a calculator or tool? Suggest what you'd like to see on our site.