Strapi customizations options : What to use when?

6th March, 2025


On its homepage, the Strapi team describes the Strapi CMS as the most customizable Headless CMS. A huge number of customization options is one of the reasons behind Strapi’s popularity. But, back when I started to dabble with Strapi, I found this large number of customization options overwhelming. My biggest worry was:

Given a certain CMS requirement, was I choosing the right Strapi option to implement the requirement?

Now, having worked on various Strapi CMS implementations, I’m trying to answer this for others having the same dilemma.

Strapi’s Customization Options

Before talking about mapping requirements to Strapi customization options, let’s list down some customization features that come with Strapi:

  • Policies - These are functions that execute before the code related to a Rest API is executed. These are mostly used to secure the Rest APIs. For example - a function that determines if the user making the Rest API request is allowed access to that API route.
  • Middlewares - These are functions that are triggered whenever a certain HTTP request is received by the CMS. For example - a function that writes to an audit log whenever a REST API request to fetch a file is received.
  • Life-cycle hooks / document service middleware - These are functions that run when CRUD operation for a Strapi collection is performed. For example - a function that ensures that the field slug is autopopulated if empty when a record for a page is inserted.
  • Scheduled jobs - These are tasks that need to run at a specific time of the day / week / month. For example - a job that sends scheduled email to all the subscribers.
  • Custom APIs - Rest APIs that can be built to serve custom requirements. Like a Rest API URL to validate a token.
  • Plugins - This is a capability to extend Strapi Admin's features. For example - a plugin that enables exporting data from Strapi into a CSV file via the Strapi Admin UI.
  • Overriding core Strapi Admin functionalities - This is a capability that enables overriding the default behavior of Strapi's core functionalities. For example - adding an additional field for file upload that determines whether the uploaded file should be publicly accessible or not.

Securing API routes

Whenever custom logic needs to be run to determine if a certain API request should be served or not, Strapi policy is the most suitable option to implement such a requirement.

For example - to disallow registered users from a certain list of domains from making /api/downloadables/ request, the below-coded policy can be leveraged:

//file : src/api/downloadables/policies/disallow-prohibited-domains.js

module.exports = async (policyContext, config, { strapi }) => {
    const blockedDomains = await fetchBlockedDomains();
    if (policyContext.state.user) { 
      const email = policyContext.state.user.email;
      if (email && email.includes('@') 
        && !blockedDomains.includes(email.split("@").pop()))
        return true;
      else
        return false;
    }
    return false;
};

The policy can be attached to the API route as following:

//file : src/api/downloadables/routes/custom-route.js
module.exports = {
  routes: [
    {
      method: 'GET',
      path: '/downloadables',
      handler: 'Downloadables.find',
      config: {
        policies: ['api::disallow-prohibited-domains.disallow-prohibited-domains']
      }
    }
  ]
}

A policy can be collection-specific (like the one coded above). Such policies can be attached to one or more routes of that collection. Or, it can be a global policy that can be attached to any of the collection routes.

A major limitation of a policy is that it can only return true or false to determine if the request should be served or not. It cannot be leveraged to control what data the request should serve. With the above example, if we had to determine what downloadable to return based on the requesting user’s email address, a Strapi policy cannot be used to implement this.

Altering the default Rest API behavior

For every collection, Strapi provides Rest APIs for CRUD operations on the records of these collections out-of-the-box. And one of the most common customization requirements is to alter the behavior of one or more of these built-in Rest APIs. Such customizations can be implemented via route-specific middleware, document services middleware or lifecycle hooks. But there’s always one option better than the other, depending on the context.

Some example customizations:

  • Pass data to an external system when a record is created / edited via the Rest API.
  • Perform input validation when the Rest API is invoked to create / update data.
  • Send an email when the Rest API request is received to update a certain data (e.g., acceptance of the terms and conditions).
  • Generate access log when a certain record is fetched.
  • Have a server-side API caching layer.

The most flexible and maintainable way to implement any of the above customizations is via route-specific middleware.

For example - to send an email whenever a user accepts the terms and conditions:

//file : src/api/profile/middlewares/notify-terms-accepted.js
module.exports = (config, { strapi })=> {
    return async (ctx, next) => {
      await next();
      if (ctx.request.body?.data?.terms_accepted) {
        const userEmail = ctx.request.body?.data?.email;
        sendTermsAcceptedEmail(userEmail);
      }
    };
}      

And, we could attach this middleware to specific routes as following:

//file : src/api/profile/routes/profile.js
const defaultRouter = createCoreRouter('api::profile.profile', {
    config: {
        update:  {
            middlewares: [ 'api::profile.notify-terms-accepted']
        },
        create: {
            middlewares: [ 'api::profile.notify-terms-accepted']
        }
    }
});

The middleware coded above is collection-specific and can be applied to routes belonging to the collection. But, we can also create global middleware that can be applied to multiple requests (API requests from the frontend or admin API from the admin UI).

Benefits of route-specific middleware

Strapi also provides life-cycle hooks (with v4) and document services middleware (with v5) to customize a collection create or update behavior. But I have found route-specific middleware to be my go-to approach whenever feasible because:

  • Route-specific middleware are a koa capability (a framework Strapi is built on). As a result, I do not expect Strapi to move away from these (like they did with lifecycle hooks with Strapi v4 -> v5)
  • Route-specific middleware can be applied to customize not just Rest API behavior but also Admin API behavior (see example).

The only place where life-cycle hooks or document services middleware would be preferred over route-specific middleware is when the customization also needs to work for in-code CRUD operation with records. But I have not come across such customization requirements.

Adding Custom APIs

Some features require APIs that function beyond the regular CRUD operations. The most suitable way to implement such a functionality is by adding them as custom routes to the Strapi setup. Below are some examples that would work well as custom APIs:

  • /search API to fetch and serve search results from an upstream search index.
  • /generate-key API to generate and serve a secure token to be used for future requests authorization.
  • /make-purchase API to update orders and inventory collection records.

A custom route can be declared as following:

//file : src/api/security/routes/custom-routes.js

module.exports = {
  routes: [
    {
      method: "GET",
      path: "/security/generate-key",
      handler: "security.generateKey",
    },
  ],
};

And, a custom controller for the custom route can be defined as below:

//file : src/api/security/controllers/secure.js

"use strict";

const { createCoreController } = require("@strapi/strapi").factories;

module.exports = createCoreController("api::secure.secure", ({ strapi }) => ({
  async generateKey(ctx) {
      //custom code for secure key generation goes in here.
  },
}));

Running scheduled jobs

A Strapi setup often requires certain jobs to be run periodically. Some examples:

  • Run an indexing job to add the records added or updated in the last 1 hour to search.
  • Send newsletter email to all the subscribers at a scheduled time.
  • Purge or archive old audit logs once a week.

Strapi enables a cron-like configuration to execute such functions. Here’s a Strapi cron setup to execute performIndexingForSearch every day at 04:00 AM. This function (with access to the strapi object) can query data from Strapi collections and pass to the external search indexing setup.

// file : config/cron-tasks.js
module.exports = {
    performIndexingForSearch: {
      task: async({strapi}) => {
          console.log('Starting cron job : performIndexingForSearch')
          return await performIndexingForSearch({strapi});  
      },
      options: {
        rule: "00 04 * * *",
      },
    },
};

Extending the Strapi Admin features

The teams that use the Strapi Admin UI (example - content, marketing, and mechandising teams) may request for additional admin capabilities to make their work easier. Such extension of Strapi admin capabilities can be achieved by writing plugins using Strapi’s plugin SDK.

Here are some example requirements that would be ideal to implement as a Strapi plugin:

  • Adding custom field types beyond the ones provided by Strapi’s content manager (Google map location selector, select control with pagination and autocomplete, etc.).
  • Custom Admin UI to review and resend bounced emails.
  • Enable users of the admin UI to configure what Strapi data gets sent to external systems.

Strapi Marketplace lists many plugins developed by the Strapi company as well as third-party developers. It is always ideal to check if a custom requirement to extend the Strapi Admin feature can be solved by using one of these plugins.

Overriding the core Strapi Admin functionalities

The teams that use the Strapi Admin UI may request for the core Strapi functionalities to work differently. Or, they may encounter bugs within their Strapi setup that aren’t yet addressed by the Strapi organization. The only way to address such situations is by overriding the core Strapi code.

Here are some examples of such requirements:

  • Change of logic for new user registration, login or forgot password (served by Strapi’s Users & Permissions plugin).
  • Modify the Content Manager search or filter logic.
  • Add fields to files uploaded via the Strapi media library.

Overriding of server-side logic (like the logic for new user registration listed above) can be achieved by adding the changes within the src/extension folder.

Overriding the Admin UI features (like modifying the content manager search logic) requires making modifications within a fork of the @strapi/strapi package and using the build output files from this forked version (as detailed here).

Overriding the core Strapi Admin UI features is maintenance heavy, as it needs to be manually handled during every Strapi upgrade. As a result, it is recommended to not override core Strapi features unless absolutely inevitable.

Conclusion

Implementing a requirement with the right customization option can go a long way in helping ensure:

  • The customization is bug-free.
  • The changes can be easily extended in the future as required.
  • The changes can be easily migrated during Strapi version upgrades.
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-2025 Tezify All Rights Reserved. Created in India. GSTIN : 24BBQPS3732P1ZW.