Strapi Lifecycle Hooks — beforeCreate, afterUpdate, and Model Lifecycle
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
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.
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.
Modifying data incorrectly in before hooks. The
dataobject inbeforeCreateandbeforeUpdateis the raw input. Modify it directly. Do not reassign the entiredataobject.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.
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
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.
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.
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.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
Mini Project
Your task: Build a complete lifecycle-driven content management system.
- Create a "Document" content type with fields: title (string, required), content (richtext), slug (string), status (enumeration: draft, review, published), views (integer).
- 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
- Test all hooks by creating, updating, publishing, and deleting documents through the API.
- 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:
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro