Strapi lifecycle hooks
A practical use case to learn Strapi lifecycle hooks "by doing"
In this post, I want to show a practical implementation of Strapi lifecycle hooks. These are functions that are automatically triggered by Strapi after or before a certain event occurs that involves content types, for example creating or deleting a content type item. I will do this while solving a real-world issue I faced while working on the "developer blog" project for my Complete Strapi Course. If you want to follow along, here I link the repo at the commit stage before adding the lifecycle hooks code: link
If you prefer learning by videos, here you have it!
The issue
In my blog Strapi app, I wanted to create a simple Post content type, that was meant to have an authors
relation field to keep track of the admin user that has created each post, plus additional admin users that may be co-authors. You can check this implementation in the repo link reported above. Now the problem is that, for security reasons, admin users are not returned by the Strapi API, so it was impossible to retrieve the authors
field from the client.
The solution with lifecycle hooks
Using lifecycle hooks we can put in place a workaround for this issue. We can create a new dedicated collection type for Author
s to associate with the post, which will be free from any security concerns and so free to be returned as an API response. Spoiler: the Author
is just a convenience duplicate of the Admin User
, so we'll replicate the fields accordingly as follows:
As you can see I also added a 1-to-1 relation with the Admin User
, which is necessary to keep the link between the 2 collections and to retrieve the Author
related to a certain Admin User
. But again, we still want instances of this new Author
type to be really in sync with Admin Users, that are the only ones who should be able to create posts.
With that said, here's what we're going to do using lifecycle hooks to cut manual creation/update of entities:
listen to the events of creating and updating instances of
Admin User
, to automatically create or update an instance ofAuthor
; optionally this may be done also for thedelete
event (but I don't feel that necessary)add a content-type lifecycle hook for
Post
s to automatically assign theAdmin User
who's creating a newPost
(or, better, the correspondingAuthor
instance) to the newly createdPost
as anauthor
Alternative lifecycle hooks strategies
The hooks mentioned above that we're going to create allow me to introduce the 2 strategies that Strapi offers to subscribe to events, as mentioned in the docs:
the first one is to create a lifecycle file specific to a content type: that's the obvious way to go if you're dealing with API you (or your plugins) have introduced in the app, so we'll do this for the
Post
hookthe alternative way, which seems simpler for the
Admin User
content type, is to directly subscribe to events at the database layer: we'll apply this for theAdmin User
hooks
Subscribing to database layer events
Let's start creating the hooks for our Admin User
type. For this, we edit the bootstrap
function inside the src/index.js
file. Let's first add the afterCreate hook: see the comments in the code below.
// ./src/index.js
module.exports = {
// Omitted
bootstrap({ strapi }) {
// we listen to lifecycle events...
strapi.db.lifecycles.subscribe({
// only listen to events for type with this UID
models: ["admin::user"],
// after creating a new Admin
afterCreate: async ({ result }) => {
// take all attributes of the created instance...
const {
id,
firstname,
lastname,
email,
username,
createdAt,
updatedAt,
} = result;
await strapi.service("api::author.author").create({
// and use those to create a corresponding Author
data: {
firstname,
lastname,
email,
username,
createdAt,
updatedAt,
// note how I assign the new Admin User to the Author
admin_user: [id],
},
});
},
});
},
};
As you can see we have subscribed to the event triggered after the creation of a new entity of admin::user
type (what I've called Admin User
until now) and we perform a simple operation of taking the information about the newly created instance and using that to create a corresponding Author
instance, also with a relation with the Admin User
.
Now let's do a very similar operation for updates:
// ./src/index.js
module.exports = {
// Omitted
bootstrap({ strapi }) {
strapi.db.lifecycles.subscribe({
models: ["admin::user"],
afterCreate: async ({ result }) => {
// omitted
},
afterUpdate: async ({ result }) => {
// we firstly get the ID of the Author that corresponds
// to the Admin User that's been just updated
const correspondingAuthor = (
await strapi.service("api::author.author").find({
admin_user: [result.id],
})
).results[0];
// and we update accordingly the corresponding Author
// with the updated properties
const { firstname, lastname, email,
username, createdAt, updatedAt } = result;
await strapi
.service("api::author.author")
.update(correspondingAuthor.id, {
data: {
firstname,
lastname,
email,
username,
updatedAt,
},
});
},
});
},
};
Now you can test that new Author
s are created/updated upon creating/updating Admin User
s. Again, we could extend this to the deletion case, but I don't think this is necessary for my use case.
Content type lifecycles
Now to the last step: ensuring that each Post
that gets created gets the Author
corresponding to the Admin User
who's creating that post in the authors
relation field. For this, we have to create a lifecycles.js
file inside the ./src/api/post/content-types/post/
folder, as follows:
// ./src/api/post/content-types/post/lifecycles.js
module.exports = {
beforeCreate: async ({ params }) => {
// find the Admin User who created the post
const adminUserId = params.data.createdBy;
// find the corresponding Author
const author = (
await strapi.entityService.findMany("api::author.author", {
filters: {
admin_user: [adminUserId],
},
})
)[0];
// update the data payload of the request for creating the new post
// by adding the proper Author to the `authors` relationship
params.data.authors.connect =
[...params.data.authors.connect, author.id];
},
};
Please note that:
you don't have to specify the content type (or model) you're referring to, because we are inside a content type specific lifecycle file
in this case, we didn't access the
result
object, that clearly exists only inafter*
methods: here we still don't have a result, but we can handle the payload of the incoming post creation request, and in fact, we alter it in the last line of this code (that'sparams.data
)in order to find the correct
Author
for the new post, we had to use a relational field as a filtering method: that's why I used the lower Entity Service API instead of the usual Service API I used in previous stepswhen editing the
params.data.authors
field of the payload, consider that the array of data is expected to be nested inside aconnect
subfield
Conclusions and acknowledgments
That's it; we have successfully re-implemented the Post-Author relationship more robustly and took advantage of Strapi's lifecycle hooks for not having to perform any manual tasks on our content types (besides writing the post of course ๐). Please let me know in the comments about any feedback.
I want to thank the following users from the Strapi Discord Server who suggested such a solution (in record time!): KevinRI and Derrick Mehaffy from the Strapi team.
Source code and Strapi course
The updated source code is available here. As I mentioned, this is the repo of the project for my Complete Strapi Course hosted on Udemy. In case you're interested, that's a 14+ hrs course to learn everything about Strapi, from the basics to very advanced topics such as plugin development and deployment.