My experience with upgrading Strapi v4 to v5

11th Jan, 2025

With the release of version 5, Strapi introduced some major changes:

  • support for content versioning
  • changes in API response format
  • plugin development-related changes (new SDK, removal of the helper library).

Our Strapi v4 setup consisted of:

  • ~150 components used within ~100 collections.
  • 10 of our collections with custom routes, lifecycle hooks or route-specific middlewares.
  • 12 custom global middlewares for aspects like audit, access control, custom admin UI input validations, and integrations to take data out of Strapi to other sub-systems.
  • 4 in-house custom plugins to enable integration with other sub-systems and custom-UI to suite specific workflow requirements.

Our Next.js frontend consumed the Strapi APIs (Rest APIs and GraphQL requests) to render the content on our website.

With the v4 → v5 upgrade, we wanted to:

  • Ensure our setup functioned as earlier post-upgrade.
  • Deploy the upgrade with minimal downtime.

1. Upgrading the default Strapi code

Our Strapi code could be divided into two categories:

  • The default Strapi code for our collections and components (schema.json, route, controller, services files with no custom code).
  • Our Strapi customization code (custom routes, middlewares, route policies, lifecycle hooks, custom plugins).

Our goal for this step was to just upgrade the default Strapi code to v5.

1.1. Upgrading to the latest v4 version

Prior to the major version upgrade, Strapi required us to first upgrade to the latest v4 version. For us, this meant migrating from v4.24.2 to v4.25.13. We could achieve this with the minor upgrade command npx @strapi/upgrade minor.

After the upgrade, we were able to start our local Strapi setup with yarn develop and verify the changes. This was straight-forward since the upgrade command took only a few minutes to run, and we did not run into any upgrade or functional regression issues.

1.2. Upgrading to the v5 version

This required us to run the command npx @strapi/upgrade major. The command executed in a few seconds, but the step “Installing dependencies” kept on failing for us.

To overcome the command failure, we removed all our in-house plugins from the code (from the src/plugins directory) and disabled the plugins from config/plugin.js. With the removal of plugins, the major upgrade command ran without any errors for us. However, we noticed two things:

  • The upgrade command commented all our lifecycles.js code.
  • The entityService calls within our code were replaced by documents service calls. And all the update and findOne calls were left with a __TODO__ mark like below:

For example, the upgrade command changed a call within one of our cron jobs:

const updateStatus = await strapi.entityService.update('api::alert.alert', 
    rec.id, 
    {data: { alert_status: 'sent'}});

to

const updateStatus = await strapi.documents('api::alert.alert').update({
      documentId: "__TODO__",
      data: { alert_status: 'sent'}
    });

We decided to address the code commented by the Strapi upgrade command later when handling our code customizations.

2. Migrating our Strapi data

Our Strapi v4 data resided within Postgres. And our goal here was to have the same data available on our Strapi v5 setup. To achieve this, we simply ran the yarn develop command on our v5 setup.

But this failed for us because of two issues within our setup:

  • We had a collection named documents causing an error during Strapi’s data migration. I think this happened because of the name collision with the new documents services.
  • One of our collections had a field named filters. While this worked without issues on Strapi v4, this caused name collisions on Strapi v5.

To overcome these issues, we renamed the documents collection and the filters field within our Strapi v4 setup. However, renaming these would result in losing the data within this collection and field. So we had to:

  • Go back to our Strapi v4 setup.
  • Create a new collection and field with the different name.
  • Write custom scripts to copy the data over to the new collection and field.
  • Remove the old documents collection and filters field.
  • Re-execute the code upgrade command (detailed in #1.2 above).

Once the above issues were resolved, the yarn develop command:

  • successfully migrated our data to Strapi v5 (in about ~30 minutes)
  • had the development version of Strapi v5 running

This was a checkpoint where we had our schema and data migrated to Strapi v5. At this point, we could log in to Strapi v5 locally and verify our collections and components with the populated data. We performed a thorough functional check to ensure our complicated data structures (dynamic zones, collections within collections, many-to-many relationships) were migrated adequately.

Note: In my experience, the data migration code from Strapi for v4 → v5 is notably better & solid as compared to the same with Strapi v3 → v4, which was riddled in complications and edge-case bugs.

3. Upgrading our Strapi customizations

Once we had a locally running Strapi instance where we could see all our data, we worked on incrementally moving our Strapi customizations to this v5 setup, since all of this had to be done manually.

3.1. Upgrading the code for our in-house plugins

With v5, Strapi has made significant changes with respect to plugins that affected our setup:

  • The @strapi/helper-plugin has been removed. Our plugin code commonly used modules like request, auth, prefixPluginTranslations from this library. We manually rewrote all such instances to replace their usage with v5 equivalents.
  • The Admin UI for our plugins leveraged tailwind.css by importing the style sheet via import 'styles/main.css';. Strapi v5 does not allow importing CSS (details here). This required us to rewrite our plugins' UI styling code.
  • The move from id to documentId (covered in detail later) also affected API requests & responses for our plugin code. The code related to fetching and serving data was rewritten to accommodate these changes.

3.2. Upgrading the frontend API requests / responses

Strapi v5 has flattened the Graphql request and response format. This meant we had to make changes in the data-fetching layer of our frontend code. While these changes were large in volume (we had hundreds of Graphql requests), they were straight-forward to apply.

3.3. Moving from id to documentId

With the introduction of content versioning, id is no longer the unique identifier for a record within a Strapi collection. Instead, Strapi expects us to use documentId when fetching, updating, or deleting an individual record.

We were required to manually make the changes for this to work within:

  • Lifecycle events code
  • Middlewares (route-specific and global)
  • Route policies
  • Cron jobs
  • In-house plugins

With our Strapi v4 setup, we had also used the record’s id field as a unique identifier when passing data to other sub-systems within our setup. With Strapi’s move to documentId, we were required to:

  • Rewrite our integration and re-run the synchronization with other sub-systems wherever feasible.
  • Create a new field legacy_id to store the legacy id values wherever we could not update the historical records in the other sub-systems.

4. Testing

Once all our Strapi code customizations were upgraded to v5, we now had:

  • a fully functional website running on top of v5
  • integrated sub-systems working with Strapi v5
  • customizations like event hooks, crons, access-control policies running on our Strapi v5 setup

With this fully-functional setup, we:

  • Updated our unit tests and executed them to ensure no regression
  • Undertook thorough manual testing
  • Re-executed the data migration step with newer data dumps to ensure no anomaly

5. Go-live

We took our Strapi v5 setup live with:

  • No downtime for our frontend / website / read-only functionalities as it kept running on Strapi v4 during the data migration step.
  • An hour’s downtime for our Strapi admin functionalities and create-update-delete functionalities, since this is when data migration was executing.

The data migration step had been executed multiple times during our testing phase to ensure a predictable go-live. Once the data migration and the following verification steps were complete, we leveraged the DNS switch to direct our user traffic to the new Strapi v5 setup.

6. Effort required for the upgrade

A common query I received from readers of the v3 → v4 upgrade blog post was the amount of effort that upgrade took. So, I tracked the hours I spent on executing the v4 → v5 upgrade.

  • Upgrading of the default Strapi code and the data migration (items #1 and #2 in this post) took ~40 hours.
  • Updating the fetching logic for the frontend code consumed ~40 hours.
  • Updating the plugins code and all the other customizations consumed ~120 hours.
  • There were a few days of additional effort for updating Jest unit tests, functional testing, replaying the go-live day data migration, merging branches, etc.

The overall upgrade took ~6 weeks of full-time effort.

7. Conclusion

With the availability of solid data migration and code upgrade commands, we found the default Strapi v4 → v5 upgrade to be straight forward.

However, with multiple fundamental changes under the hood, upgrading Strapi customizations and plugins code was cumbersome and time-consuming.

Also, since we relied on Strapi v4’s id field as the unique identifier for integrating with external systems, Strapi’s shift to using documentId required us to handle not just code changes but also legacy data aspects.

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.