How we setup input validations for components in Strapi

6th July, 2023

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).

Adding custom input validations for fields within components for a collection


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:

Data inputted for component fields isn't directly accessible to the collection 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);
  }

Data inputted for component fields is accessible via ctx.request.body but cannot be used for input validations

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:

Data inputted for component fields is accessible via ctx.request.body but cannot be used for input validations

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:

Strapi internally treats the components as their own content-types. And, Strapi executes the create / update / delete for the component before executing the collection lifecycle.

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 a beforeCreate or beforeUpdate 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 a beforeCreate or beforeUpdate 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.

Punit Sethi
Punit Sethi
My tryst with Strapi:

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.

Read my other posts on customizing Strapi.


Copyright (c) 2017-2024 Tezify All Rights Reserved. Created in India. GSTIN : 24BBQPS3732P1ZW.