How we setup custom input validations for components in Strapi
Note : If you are just interested in the code changes to make this customization happen, please check out this github repo.
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
statusfield 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 :
datein the example above) via abeforeCreateorbeforeUpdatehook 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_ratein the example above) via abeforeCreateorbeforeUpdatehook, 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.
Need help with Strapi setup, upgrade, or customization?
I've been a Strapi specialist since 2021 (v3).
My Strapi work includes:
- Published 2 plugins on the Strapi marketplace
- Written 4+ articles featured in the official Strapi newsletter
- Contributed fixes to the Strapi core repo
- Implemented, upgraded, and customized Strapi setups for multiple teams
My Clients Include:
Or email: punit@tezify.com
- Published 2 plugins on the Strapi marketplace
- Written 4+ articles featured in the official Strapi newsletter
- Contributed fixes to the Strapi core repo
- Implemented, upgraded, and customized Strapi setups for multiple teams
Hire Me