Skip to content

Strapi Lifecycle Hooks — beforeCreate, afterUpdate, and Model Lifecycle

DodaTech Updated 2026-06-28 9 min read

In this tutorial, you will learn how to use Strapi lifecycle hooks — functions that run automatically at specific points in a content entry's lifecycle, such as before creation, after update, and before deletion — enabling custom logic without overriding controllers.

What You'll Learn

  • What lifecycle hooks are and when they execute
  • The available lifecycle hooks: beforeCreate, afterCreate, beforeUpdate, afterUpdate, beforeDelete, afterDelete
  • How to write lifecycle hooks for specific content types
  • How to access the current entry and event data in hooks
  • Common use cases: slug generation, data transformation, notifications
  • How to avoid common pitfalls with async hooks

Why It Matters

Lifecycle hooks let you add custom behavior without modifying controllers or services. Want to auto-generate a slug from the title? Lifecycle hook. Want to send an email when content is published? Lifecycle hook. Want to log all changes to an audit table? Lifecycle hook. They are the cleanest way to run code at specific moments in the content lifecycle.

Real-World Use

A recipe site needs to: auto-generate URL slugs from recipe titles (beforeCreate), check for duplicate titles (beforeCreate), log all changes for audit (afterUpdate), send notifications to subscribers when a recipe is published (afterUpdate with publishedAt check), and clean up related data when a recipe is deleted (afterDelete).

Learning Path

flowchart LR
  A["Internationalization"] --> B["Lifecycle Hooks
-- You are here"]:::current B --> C["Custom Middleware"] C --> D["Webhooks"] classDef current fill:#4945ff,color:#fff,stroke-width:2px

What Are Lifecycle Hooks?

Lifecycle hooks are functions that Strapi executes automatically at specific points during an entry's lifecycle.

Entry lifecycle:
1. User submits create/update/delete
2. beforeCreate / beforeUpdate / beforeDelete — runs BEFORE database operation
3. Database operation (insert/update/delete)
4. afterCreate / afterUpdate / afterDelete — runs AFTER database operation
5. Response sent to the client

The "before" hooks let you modify the data before it is saved. The "after" hooks let you perform actions after the data is saved.

Available Hooks

Each content type supports six lifecycle hooks:

// Lifecycle hooks for an Article content type
// src/api/article/content-types/article/lifecycle.js

module.exports = {
  // Before creating a new entry
  beforeCreate(event) {
    const { data } = event.params;
    // Modify or validate data
  },

  // After creating a new entry
  afterCreate(event) {
    const { result } = event;
    // Do something with the created entry
  },

  // Before updating an existing entry
  beforeUpdate(event) {
    const { data, where } = event.params;
    // Modify update data
  },

  // After updating an existing entry
  afterUpdate(event) {
    const { result } = event;
    // React to the update
  },

  // Before deleting an entry
  beforeDelete(event) {
    const { where } = event.params;
    // Check if deletion should proceed
  },

  // After deleting an entry
  afterDelete(event) {
    const { result } = event;
    // Clean up related data
  },
};

The Event Object

Each hook receives an event object with useful properties:

module.exports = {
  beforeCreate(event) {
    // event.state — access to Strapi state
    // event.action — "beforeCreate"
    // event.model — the content type UID (e.g., "api::article.article")
    // event.params — parameters passed to the database operation
    //   event.params.data — the data being saved
    //   event.params.where — filters (for update/delete)
  },

  afterCreate(event) {
    // event.result — the created entry
    // event.params — same as before, but data has been processed
  },
};

Before Create Hook

The beforeCreate hook runs before the entry is saved. Use it to modify or validate data.

// src/api/article/content-types/article/lifecycle.js
module.exports = {
  beforeCreate(event) {
    const { data } = event.params;

    // Auto-generate slug from title
    if (data.title && !data.slug) {
      data.slug = data.title
        .toLowerCase()
        .replace(/[^a-z0-9]+/g, "-")
        .replace(/(^-|-$)/g, "");
    }

    // Set default values
    if (data.views === undefined) {
      data.views = 0;
    }

    // You can also throw errors to prevent creation
    if (data.title && data.title.length < 5) {
      throw new Error("Title must be at least 5 characters");
    }
  },
};

The beforeCreate hook can also check for duplicates:

beforeCreate: async (event) => {
    const { data } = event.params;

    if (data.email) {
      const existing = await strapi.db.query("api::user.user").findOne({
        where: { email: data.email },
      });

      if (existing) {
        throw new Error("A user with this email already exists");
      }
    }
  },

After Create Hook

The afterCreate hook runs after the entry is created. Use it for side effects.

// src/api/article/content-types/article/lifecycle.js
module.exports = {
  afterCreate: async (event) => {
    const { result } = event;

    // Log the creation
    strapi.log.info(`Article created: ${result.id} - ${result.title}`);

    // Send notification to admin
    try {
      await strapi.plugin("email").service("email").send({
        to: "admin@example.com",
        subject: "New article created",
        html: `<p>Article "${result.title}" has been created.</p>`,
      });
    } catch (error) {
      strapi.log.error(`Failed to send notification: ${error.message}`);
    }

    // Create an audit log entry
    await strapi.db.query("api::audit-log.audit-log").create({
      data: {
        action: "CREATE",
        contentType: "article",
        entryId: result.id,
        performedBy: "system",
      },
    });
  },
};

Before Update Hook

The beforeUpdate hook runs before an entry is updated. Use it to modify update data or validate changes.

module.exports = {
  beforeUpdate: async (event) => {
    const { data, where } = event.params;

    // Track which fields changed
    if (data.title) {
      const existing = await strapi.db.query("api::article.article").findOne({
        where: { id: where.id },
      });

      if (existing && existing.title !== data.title) {
        data._previousTitle = existing.title;
      }
    }

    // Prevent un-publishing if entry has comments
    if (data.publishedAt === null) {
      const comments = await strapi.db.query("api::comment.comment").count({
        where: { article: where.id },
      });

      if (comments > 0) {
        throw new Error("Cannot unpublish article with comments");
      }
    }
  },
};

After Update Hook

The afterUpdate hook runs after the entry is updated.

module.exports = {
  afterUpdate: async (event) => {
    const { result } = event;

    // Check if the entry was just published
    if (result.publishedAt && !result._previousPublishedAt) {
      // Send notification about new publication
      await notifySubscribers(result);
    }

    // Invalidate cache for this entry
    await clearCache(`article:${result.id}`);
  },
};

Before and After Delete Hooks

module.exports = {
  beforeDelete: async (event) => {
    const { where } = event.params;

    // Check if deletion is allowed
    const comments = await strapi.db.query("api::comment.comment").count({
      where: { article: where.id },
    });

    if (comments > 0) {
      // Delete all associated comments first
      await strapi.db.query("api::comment.comment").deleteMany({
        where: { article: where.id },
      });
    }
  },

  afterDelete: async (event) => {
    const { result } = event;

    // Clean up media files that are no longer referenced
    if (result.cover_image) {
      await strapi.plugin("upload").service("upload").remove({
        id: result.cover_image,
      });
    }

    // Log the deletion
    strapi.log.warn(`Article deleted: ${result.id} - ${result.title}`);
  },
};

Async Hooks

All hooks can be synchronous or asynchronous. Use async/await for operations like database queries or API calls.

module.exports = {
  // Async hook example
  afterCreate: async (event) => {
    const { result } = event;

    // Multiple async operations
    const [notificationResult, indexResult] = await Promise.all([
      sendNotification(result),
      updateSearchIndex(result),
    ]);

    strapi.log.info(`Post-creation tasks completed for article ${result.id}`);
  },
};

When to Use Lifecycle Hooks vs Other Approaches

Task Best Approach
Data validation Lifecycle hook (beforeCreate/beforeUpdate)
Auto-generate fields Lifecycle hook (beforeCreate)
Side effects (email, logs) Lifecycle hook (afterCreate/afterUpdate)
Complex business logic Custom service (called from lifecycle hook)
Request transformation Custom middleware
Access control Custom policy

Common Mistakes

  1. Making hooks too complex. Lifecycle hooks should be short and focused. Complex logic belongs in services. Call services from hooks instead of writing everything inline.

  2. Not handling errors in async hooks. An unhandled promise rejection in a hook can crash the Strapi Process. Always wrap async operations in try/catch.

  3. Modifying data incorrectly in before hooks. The data object in beforeCreate and beforeUpdate is the raw input. Modify it directly. Do not reassign the entire data object.

  4. Creating infinite loops. If an afterCreate hook creates or updates an entry that triggers the same hook, you get an infinite loop. Add guards to prevent re-triggering.

  5. Forgetting that hooks run on admin panel operations too. Lifecycle hooks run for both API and admin panel operations. Test hooks with both entry points.

Practice Questions

  1. What are the six lifecycle hooks available in Strapi? Answer: beforeCreate, afterCreate, beforeUpdate, afterUpdate, beforeDelete, afterDelete. Each runs at a specific point in the entry lifecycle.

  2. What is the difference between beforeCreate and afterCreate? Answer: beforeCreate runs before the database insert — you can modify or validate data there. afterCreate runs after the insert — you can trigger side effects like notifications.

  3. How do you access the created entry in an afterCreate hook? Answer: Through event.result. The result object contains the entry data including the id, all attributes, and timestamps.

  4. Challenge: Build a comprehensive lifecycle system: (1) Create a beforeCreate hook that auto-generates a slug and validates the title is unique, (2) Create an afterCreate hook that sends a welcome email and logs to audit, (3) Create a beforeUpdate hook that prevents changing the author field after the entry is 24 hours old, (4) Create an afterUpdate hook that sends a "content updated" notification to subscribers, (5) Create an afterDelete hook that cleans up associated files, (6) Test each hook and verify the behavior with both valid and invalid operations.

FAQ

Can I use lifecycle hooks on the User content type?

Yes. User is a content type like any other. Create src/extensions/users-permissions/content-types/user/lifecycle.js to hook into user creation, updates, and deletion.

Do lifecycle hooks run for bulk operations?

Bulk operations like deleteMany do not trigger lifecycle hooks. Only single-entry operations through the API or admin panel trigger hooks.

Can I prevent an entry from being saved in a before hook?

Yes. Throw an error in a beforeCreate or beforeUpdate hook. Strapi catches the error and returns it to the client without saving.

How do I access the current user in a lifecycle hook?

Lifecycle hooks do not have direct access to the request context or current user. Use event.state for additional context if passed, or query the audit trail separately.

Can I have lifecycle hooks for components?

No. Lifecycle hooks are only available on content types (collection types and single types). Components do not have independent lifecycles.

Mini Project

Your task: Build a complete lifecycle-driven content management system.

  1. Create a "Document" content type with fields: title (string, required), content (richtext), slug (string), status (enumeration: draft, review, published), views (integer).
  2. Implement lifecycle hooks:
    • beforeCreate: Auto-generate slug from title, set default status to "draft", initialize views to 0
    • afterCreate: Log creation to an audit table (create a simple AuditLog content type or use console.log)
    • beforeUpdate: If status is changing to "published", require content to be non-empty
    • afterUpdate: If status changed to "published", trigger a Webhook or log
    • afterDelete: Log the deletion
  3. Test all hooks by creating, updating, publishing, and deleting documents through the API.
  4. Verify error handling: try creating without a title (should fail from validation), try publishing without content (should fail from beforeUpdate).

What's Next

Now that you understand lifecycle hooks, proceed to Custom Middleware to learn how to intercept and process every API request and response. After that, explore Webhooks for external service integration.

Related lessons:

  • API Customization — Overriding controllers and services
  • Node.js — Understanding async/await in hooks
  • REST API — How hooks interact with API responses

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro