How we setup input validations for components in Strapi
Note 1 : If you are just interested in the code changes to make this customization happen, please check out this github repo.
Note 2 : The behavior detailed in this blog post & the provided solution is Strapi v4 specific (and would not work with Strapi v3).
A few weeks ago, I was tasked to add a custom input validation for a field exchange_rate
within our Strapi setup. This field was part of a component called payment_details
which was added to our collection called transaction
.
I had to write a validation that would ensure that the exchange_rate
entered by our Strapi admins is less than the currency exchange rate on that day (I had to fetch the currency exchange rate for the day via a third-party API).
At the outset, this looked like one of those regular changes I make to our Strapi setup using the collection lifecycle hooks.
1. Lifecycle hooks to the rescue, or not?
Lifecycle hooks have been my goto place every time I hear the term input validation
with respect to Strapi. beforeCreate
and beforeUpdate
seem to be provided by Strapi for this purpose.
To implement this, I planned to grab the data inputted for the payment
component from the event.params.data
available to the beforeCreate
and beforeUpdate
hooks. But, I observed that Strapi doesn’t pass component data to the collection lifecycle hooks. Below is the console.log(event.params.data);
output from these lifecycle hooks:
After searching on the Strapi forum and found that I could access the component data via the below (only works with Strapi v4.3.9 or later):
async beforeUpdate(event) {
const ctx = strapi.requestContext.get();
console.log(ctx.request.body);
}
Since the payment component data was now available within the lifecycle hooks, I thought it would be straight forward to code my custom
exchange_rate
validation as below:
async beforeUpdate(event) {
const ctx = strapi.requestContext.get();
const payment = ctx.request.body.payment;
if (payment?.exchange_rate &&
isExchangeRateInvalid(payment.exchange_rate))
throw new ApplicationError('Invalid payment.exchange_rate value.');
}
But, I observed an unpredictable behavior with the above lifecycle hook implementation:
The custom input validation displayed the adequate error message when the input validation failed. But, it exhibited an unexpected behavior with respect to storing the inputted values:
- Any data inputted within the component fields (even if failing the input validations) got stored in the system.
- Any data inputted within the non-component fields (like
status
field in the screenshot above) would not be stored in the system if the input validation failed.
While this behavior was confusing, it was clear that I could not use ctx.request.body
within the lifecycle hooks to setup my input validation.
2. Why lifecycle hooks cannot be used to validate component input values
The quirky behavior experienced above triggered me to delve deeper to understand what was happening here. On spending time to better understand things, I observed the following:
So, Strapi performs components related create / update before the lifecycle hooks are called. As a result:
- If we write a custom input validation for a regular field (eg :
date
in the example above) via abeforeCreate
orbeforeUpdate
hook AND if the same collection also has certain component data - the component data changes will always happen irrespective of this custom input validation passing or failing. - If we write a custom input validation for a component field (eg :
exchange_rate
in the example above) via abeforeCreate
orbeforeUpdate
hook, it will display the error if the validation fails. But, the component data changes will occur despite the error while non-component data changes will not occur.
To overcome the above detailed behavior, I needed a way to execute my input validation code before Strapi stores component data into the system.
3. Leveraging Strapi global middleware
Strapi’s global middleware allows a fine-grained control over when to execute a certain piece of code. We can use it to write code that can execute before Strapi does anything with a received request. So, I wrote a global middleware to do our input validation task:
module.exports = ()=> {
return async (ctx, next) => {
//Perform input validations only for create & update requests
if (ctx.request.method === 'PUT' || ctx.request.method === 'POST')
{
//Perform input validation only for requests coming from the Strapi Admin
//content-manager
if (ctx.request.url.includes('/content-manager/collection-types/api::') ||
ctx.request.url.includes('/content-manager/single-types/api::'))
{
const apiName = getCollectionNameFromRequestUrl(ctx.request.url);
if (Object.keys(validations).includes(apiName))
{
let data = ctx.request.body;
//call the input validation function
await validations[apiName](data);
}
}
}
//IMPORTANT : Give control back to Strapi to handle the request
//only AFTER our input validation logic execution completes.
await next();
}
}
With the above code in place, I now defined a validations
object that would contain custom validation code for my collection:
const validations = {
'transaction' : (data) => {
const { payment } = data;
if (payment?.exchange_rate &&
isExchangeRateInvalid(payment.exchange_rate))
throw new ApplicationError('Invalid payment.exchange_rate value.');
}
}
With the above detailed structure in place, as I would add new collections and custom validations for those collections, I could add to the validations
object above.
The full code for this custom global middleware can be found here.
4. Conclusion
Until Strapi provides the much-requested lifecycle hooks for components feature, using global middlewares is a way to write custom input validations for components used within our Strapi collections.
Back in mid-2021, one of my clients was having issues with their in-house CMS. Despite me being their frontend architect, they trusted me to build their CMS solution. At this point, evaluation of various frameworks, approaches and products led me to Strapi. And, I ended up implementing Strapi CMS for their data requirements.
Fast-forward to now, I have worked with multiple orgs on implementing, upgrading & customizing Strapi setups based on their requirements.