This article will cover the "bird's view" of the Strapi v3 to v4 migration process. Follow along with the migration process of our Food Advisor app to understand better what it entails before you do it in your project.
This post will cover the "bird's view" of the Strapi v3 to v4 migration process.
We will cover the general process overview, the steps you must take, and how to use the migration guide to help you succeed effectively.
This article aims to have you follow along with the migration process of our Food Advisor app to understand better what it entails before you do it in your project.
This article is based on our live stream that you can watch here. However, we wanted to make a supplementary article that you can use as a reference when needing to go over the general overview of the process.
We won't be able to cover all the nuances of the process. Still, our goal is to provide enough overview to make you comfortable with the process.
Please note that depending on how much of your project is custom will dictate the time and effort it will take to migrate your application. The more custom code you have, the more time it will take.
But to help us along the way, we have a code migration script and a database migration script to assist us in the process and documentation that covers every step of the process.
With that said, there is no "one-click migration" due to the changes that the Strapi team made between Strapi v3 to Strapi v4.
We rewrote most of Strapi's code to build a foundation to support our future features, allowing a more straightforward migration process from v4 to v5.
Note on GraphQL: Fundamentally, we used a more programmatic way to handle GraphQL. And the controllers are no longer coupled with the GraphQL resolvers.
Also, due to our new "populate" feature, you can now designate what data you would like to get from your REST API to prevent over-fetching.
Due to these two things and more flexible REST API implementation, you may have to revisit if GraphQL is still a requirement.
If GraphQL is still something you would like to use from your previous version, you would have to rewrite any of your custom resolvers with the required logic.
code migration diagram
database migration diagram
In this post, we will only cover the code migration process, but once this is complete, you will be able to move to the data migration step in a future post.
Looking at the code migration diagram, let's go over each step.
Step 1: Start with backend code migrations. (make sure you are connecting to a new database)
Step 2: Manually migrate the configs as shown in the docs.
Step 3: Use codemods.
Step 4: Update dependencies + plugins folder structure can be automatically migrated.
Step 5: If the app contained any customizations to routes, controllers, or services. it needs to be checked and done manually.
Step 6: Your app should now build.
We are now left with the final step, data migration, which we will cover in detail in a future article.
Don't worry if these steps are not yet clear; we will go through them in detail as we go through the migration process.
But you can find the documentation here to help us along in the process, which we will refer to as we make our way through this tutorial.
Strapi Migration Documentation
Before we start, you can get the code here if you want to follow along.
We will use a version of the Food Advisor app we used in the stream as a demonstration.
It is an excellent example because it has some customizations that will allow us to review how to handle the changes in your application and where to find additional help.
Let's go ahead and clone the Food Advisor Strapi instance by running the following command:
git clone https://github.com/PaulBratslavsky/demoMigration.git demomigration
After cloning the repo, change directories to demomigration.
Make sure you use node version 12 before running yarn
to install all the dependencies.
If you are using NVM, you can switch your node version by typing nvm use 12
and then run yarn && yarn seed
to seed our data.
Finally, run yarn build && yarn develop
to start the application.
After everything is done, navigate to http://localhost:1337/admin
. You should now see the registration page. Go ahead and create an admin account and log in.
Once you are logged in, you should be able to see all of the content types and data.
Great success. We now have a Strapi v3 project running that we can use to learn about the migration process.
Before we move on to the next step, let's install a copy of a Strapi 4 project that we can use as a reference. Having it handy will make a few things easier to talk about.
Make sure you create it in a different folder, not in the current project. You can do so by running the command below.
npx create-strapi-app@latest reference --quickstart
Before we start the migration process, back in our Food Advisor Strapi v3 demo project, let's remove the .git
file.
You can do so by running rm -rf .git
which will remove the previous repo, so you get to initialize your own by running git init.
Afterward, you can save the changes by running the following command.
git add .
git commit -m "initial commit"
Inside your project, you should see the .tmp
folder which will have our SQLite database called data.db
.
Let's rename it to old_data.db
. Afterward, you can run yarn develop
, which will create a new blank database for us to use, so we don't have to worry about messing up our data.
You will be prompted to create a new admin user since we are now using a new database.
Important: do not create a new user admin and login. Wait until you complete all the code migration steps to initialize the database.
It's not a big deal if you do. You will juwst have to delete it once more and reinitialize the database when running v4.
We will save the old_data.db
to use in a later post when we cover data migration.
Next, let's update the config
folder. Currently, our structure looks like the left side.
As you can see, there are differences in the folder structure from Strapi v3 to Strapi v4 below.
You can learn more about the changes in the documentation here.
But here is a brief overview of what changed.
We will make this easy on ourselves.
First, open your v3 project in file explorer, navigate to the config
folder, and delete all the items.
Then open the reference v4 project. Navigate to your config folder, select all items, and copy them to your v3 project.
Your v3 project config
folder should look like this.
After making the changes, let's save what we just did by running the following command.
git add .
git commit -m "updated the config folder"
Before we move to the next step, let's create a separate branch and switch. We need to do this before using codemods.
You can do so by running the following commands.
git branch before-codemods
git checkout before-codemods
Codemods is a script that will allow you to easily update the folder structure, content schemas, and other items to make the migration process easier.
You can learn more about it here.
In our newly created branch, let's run the following command.
npx @strapi/codemods migrate
Next, select the Application
option.
? What do you want to migrate?
❯ Application
Plugin
Only Dependencies
Next, set enter your root path for your Strapi application ./
and press enter to continue.
? What do you want to migrate? Application
? Enter the path to your Strapi application ./
You can run the following command to see the changes codemods made.
git add . && git diff --cached
Let's save our changes by typing the following command.
git commit -am "migrate API to v4 structure"
After running codemods, the next step is to update our dependencies in the package.json
file.
You can learn more here
1"dependencies": {
2 "body-scroll-lock": "^3.1.5",
3 "fs-extra": "8.1.0",
4 "knex": "0.16.3",
5 "lodash": "^4.17.5",
6 "reactour": "^1.18.0",
7 "sqlite3": "^4.0.6",
8 "unzip-stream": "^0.3.0",
9 "@strapi/strapi": "4.2.3",
10 "@strapi/admin": "4.2.3",
11 "@strapi/plugin-content-manager": "4.2.3",
12 "@strapi/plugin-content-type-builder": "4.2.3",
13 "@strapi/plugin-email": "4.2.3",
14 "@strapi/plugin-graphql": "4.2.3",
15 "@strapi/plugin-upload": "4.2.3",
16 "@strapi/plugin-users-permissions": "4.2.3",
17 "@strapi/utils": "4.2.3"
18},
We can remove the following.
1 "knex": "0.16.3",
2 "@strapi/admin": "4.2.3",
3 "@strapi/plugin-content-manager": "4.2.3",
4 "@strapi/plugin-content-type-builder": "4.2.3",
5 "@strapi/plugin-email": "4.2.3",
6 "@strapi/plugin-upload": "4.2.3",
7 "@strapi/utils": "4.2.3"
Our package.json
file should look like the following.
1"dependencies": {
2 "body-scroll-lock": "^3.1.5",
3 "fs-extra": "8.1.0",
4 "lodash": "^4.17.5",
5 "reactour": "^1.18.0",
6 "sqlite3": "^4.0.6",
7 "unzip-stream": "^0.3.0",
8 "@strapi/strapi": "4.2.3",
9 "@strapi/plugin-graphql": "4.2.3",
10 "@strapi/plugin-users-permissions": "4.2.3"
11},
Before updating our node_modules
folder, we also need to update our data.db
file that you can find in the .tmp
folder.
Go ahead and delete it.
When we run yarn develop
in the next step, it will create new data.db
file using the updated schema that was created after running codemods
We can now run the following commands to update our node_module
folder based on our new dependencies.
1 nvm use 16
2 rm -rf node_modules
3 yarn
4 yarn build
5 yarn develop
You will run into the following error since we are referencing a package whose name has been updated.
[2022-07-26 13:16:42.981] debug: ⛔️ Server wasn't able to start properly.
[2022-07-26 13:16:42.982] error: Cannot find module 'strapi-utils'
In our case, the issue is found in the src/api/universal/config/schema.graphql.js
file.
To fix the error, you will need to replace all references to 'strapi-utils'
with '@strapi/utils'
and run yarn develop
again.
Here is the updated file.
1const { sanitizeEntity } = require("@strapi/utils");
2
3module.exports = {
4 query: `
5 universalBySlug(id: ID slug: String): Universal
6 `,
7 resolver: {
8 Query: {
9 universalBySlug: {
10 resolverOf: "Universal.findOne",
11 async resolver(_, { slug }) {
12 const entity = await strapi.services.universal.findOne({ slug });
13 return sanitizeEntity(entity, { model: strapi.models.universal });
14 },
15 },
16 },
17 },
18};
After running yard develop
, you might see the following error.
[2022-07-26 13:36:08.815] debug: ⛔️ Server wasn't able to start properly.
[2022-07-26 13:36:08.816] error: Knex: run
$ npm install sqlite3 --save
To fix this, follow these steps.
yarn add sqlite3
data.db
file in the .tmp
folder.yarn develop
again.We should now see a different error, which is good.
[2022-07-26 13:45:31.590] error: Middleware "strapi::session": App keys are required. Please set app.keys in config/server.js (ex: keys: ['myKeyA', 'myKeyB'])
We now have to update our .env
file with additional variables that Strapi v4 is now requiring.
In the .env
file paste the following code.
1ADMIN_JWT_SECRET=LnEkeTvz3prxACbXGzQpWQ==JWT_SECRET=7+FHjdxRrJu7opx1NMBScw==
2JWT_SECRET=MOCWJRO8sbglc5Pxgx6s+g==
3APP_KEYS=rrigJL/NdvVc3Wu6GEm85w==,gHa+ZYdvSPpT/f8iqJewiw==,ddiMVxdXh0aZgN+OSddGSw==,jLixvXdrbKqZcUFuytFzIQ==
4API_TOKEN_SALT=m/gqLSE7A+YShQgVeA+45A==
All these errors, but this is good since we are getting new errors that are helping us figure out what we have left to migrate.
[2022-07-26 13:49:22.831] error: Error creating endpoint GET /categories: Cannot read properties of undefined (reading 'find')
TypeError: Error creating endpoint GET /categories: Cannot read properties of undefined (reading 'find')
This new error means we need to update our routes to use the strapi factory functions.
In the future, this is something that codemods will handle.
But at the moment, we have to do this manually, which also applies to our controllers and services.
We are getting closer. It is time to update our routes using Strapi's factory function to generate our routes.
You can read more about the difference between v3 and v4 routes here.
Let's take a quick look at our api
folder to see all our content types where we will have to make the changes.
We will have to update the routes, controllers, and services in each content type.
note: in the future, this will be done with codemods, but as of this writing, we have to make these changes manually.
Let's use the restaurant
folder as our example. What we will do here is what you must do in all other folders in the api
folder.
Let's look at the restaurant.js
file inside the routes
folder.
restaurant/routes/restaurant.js
1module.exports = {
2 routes: [
3 {
4 method: "GET",
5 path: "/restaurants",
6 handler: "Restaurant.find",
7 config: { policies: [] },
8 },
9 {
10 method: "GET",
11 path: "/restaurants/:id",
12 handler: "Restaurant.findOne",
13 config: { policies: [] },
14 },
15 {
16 method: "POST",
17 path: "/restaurants",
18 handler: "Restaurant.create",
19 config: { policies: [] },
20 },
21 {
22 method: "PUT",
23 path: "/restaurants/:id",
24 handler: "Restaurant.update",
25 config: { policies: [] },
26 },
27 {
28 method: "DELETE",
29 path: "/restaurants/:id",
30 handler: "Restaurant.delete",
31 config: { policies: [] },
32 },
33 ],
34};
As we can see, this is how our generic routes were defined in Strapi v3. We will need to change this to use Strapi's factory function.
You can learn more here.
Here is an example of how we define routes in Strapi v4.
1// path: ./src/api/<content-type-name>/routes/<router-name>.js
2
3const { createCoreRouter } = require("@strapi/strapi").factories;
4
5module.exports = createCoreRouter("api::api-name.content-type-name");
Let's update our restaurant routes using the above example.
Our restaurant.js
file inside the routes
folder should look like this.
restaurant/routes/restaurant.js
1// path: ./src/api/restaurant/routes/restaurant.js
2
3const { createCoreRouter } = require("@strapi/strapi").factories;
4
5module.exports = createCoreRouter("api::restaurant.restaurant");
Make sure you replace the appropriate api-name
and content-type-name
. It will be the same as the folder name. In this case, it is restaurant
.
Make sure you replace the remaining routes in the rest of the content types.
I will link to a repo where you can see the final changes just in case you run into issues.
After replacing all the routes, you will have a new error when you run yarn develop
.
[2022-07-26 18:15:46.325] error: Error creating endpoint GET /categories: Handler not found "api::category.category.find"
Error: Error creating endpoint GET /categories: Handler not found "api::category.category.find"
This error means we cannot find the proper controllers, so let's fix that in the next section.
We have to update the controllers to use Strapi's factory function to generate our controllers.
Go inside the restaurant/controllers
folder and look inside the restaurant.js
file.
You will still see the old code from Strapi v3.
1module.exports = {
2 find: async (ctx) => {
3 let restaurants;
4
5 if (ctx.query._q) {
6 restaurants = await strapi.api.restaurant.services.restaurant.search(
7 ctx.query
8 );
9 } else {
10 restaurants = await strapi.api.restaurant.services.restaurant.find(
11 ctx.query
12 );
13 }
14
15 restaurants = await Promise.all(
16 restaurants.map(async (restaurant) => {
17 restaurant.note = await strapi.api.review.services.review.average(
18 restaurant.id
19 );
20
21 return restaurant;
22 })
23 );
24
25 return restaurants;
26 },
27
28 findOne: async (ctx) => {
29 const { id } = ctx.params;
30 let restaurant = await strapi.api.restaurant.services.restaurant.findOne({
31 id,
32 });
33
34 if (!restaurant) {
35 return ctx.notFound();
36 }
37
38 restaurant.note = await strapi.api.review.services.review.average(
39 restaurant.id
40 );
41
42 let noteDetails = await strapi
43 .query("review")
44 .model.query(function (qb) {
45 qb.where("restaurant", "=", restaurant.id);
46 qb.groupBy("note");
47 qb.select("note");
48 qb.count();
49 })
50 .fetchAll()
51 .then((res) => res.toJSON());
52
53 restaurant.noteDetails = [];
54
55 for (let i = 5; i > 0; i--) {
56 let detail = noteDetails.find((detail) => {
57 return detail.note === i;
58 });
59
60 if (detail) {
61 detail = {
62 note: detail.note,
63 count: detail["count(*)"],
64 };
65 } else {
66 detail = {
67 note: i,
68 count: 0,
69 };
70 }
71
72 restaurant.noteDetails.push(detail);
73 }
74
75 return restaurant;
76 },
77};
Oh no, it's a custom controller, what are we going to do?
For now, we will comment it out, and replace it with a generic controller using the Strapi's factory function as it is mentioned here.
We will come back later and talk about customizations. But for now, let's at least get to a point where we can build our app without errors.
Below is the example of a generic controller.
1// path: ./src/api/<content-type-name>/controllers/<controller-name>.js
2
3const { createCoreController } = require("@strapi/strapi").factories;
4
5module.exports = createCoreController("api::api-name.content-type-name");
As you can see, it's similar to the changes we made in the routes.
Generic routes, controllers, and services are now generated via factory functions.
Inside the restaurant/controllers/Restaurant.js
file, let's make the following changes.
1// path: ./src/api/restaurant/controllers/restaurant.js
2
3const { createCoreController } = require("@strapi/strapi").factories;
4
5module.exports = createCoreController("api::restaurant.restaurant");
6
7// will come back to this later
8
9// module.exports = {
10// find: async (ctx) => {
11// let restaurants;
12
13// if (ctx.query._q) {
14// restaurants = await strapi.api.restaurant.services.restaurant.search(ctx.query);
15// } else {
16// restaurants = await strapi.api.restaurant.services.restaurant.find(ctx.query);
17// }
18
19// restaurants = await Promise.all(
20// restaurants.map(async (restaurant) => {
21// restaurant.note = await strapi.api.review.services.review.average(restaurant.id);
22
23// return restaurant;
24// })
25// );
26
27// return restaurants;
28// },
29
30// findOne: async (ctx) => {
31// const { id } = ctx.params;
32// let restaurant = await strapi.api.restaurant.services.restaurant.findOne({ id });
33
34// if (!restaurant) {
35// return ctx.notFound();
36// }
37
38// restaurant.note = await strapi.api.review.services.review.average(restaurant.id);
39
40// let noteDetails = await strapi
41// .query('review')
42// .model.query(function (qb) {
43// qb.where('restaurant', '=', restaurant.id);
44// qb.groupBy('note');
45// qb.select('note');
46// qb.count();
47// })
48// .fetchAll()
49// .then((res) => res.toJSON());
50
51// restaurant.noteDetails = [];
52
53// for (let i = 5; i > 0; i--) {
54// let detail = noteDetails.find((detail) => {
55// return detail.note === i;
56// });
57
58// if (detail) {
59// detail = {
60// note: detail.note,
61// count: detail['count(*)'],
62// };
63// } else {
64// detail = {
65// note: i,
66// count: 0,
67// };
68// }
69
70// restaurant.noteDetails.push(detail);
71// }
72
73// return restaurant;
74// }
75// };
Make sure you replace the appropriate api-name
and content-type-name
. It will be the same as the folder name. In this case, it is restaurant
.
Make sure you replace the remaining controllers in the rest of the content types.
If there is a custom controller, for the time being, comment out the code, we will revisit this in a bit.
Finally, let's update our services to use Strapi's factory functions.
You can learn more here.
But the pattern is similar to what we just did for routes
and controllers
.
Check out the code below for an example of a generic service.
1// path: ./src/api/<content-type-name>/services/<service-name>.js
2
3const { createCoreService } = require("@strapi/strapi").factories;
4
5module.exports = createCoreService("api::api-name.content-type-name");
Go inside the restaurant/services
folder and look inside the Restaurant.js
file.
You will see the following code.
1module.exports = ({ strapi }) => {
2 return {};
3};
Let's replace it with the following code.
1// path: ./src/api/restaurant/services/restaurant.js
2
3const { createCoreService } = require("@strapi/strapi").factories;
4
5module.exports = createCoreService("api::restaurant.restaurant");
Make sure you replace the remaining services in the rest of the content types.
You will notice that review/services/Review.js
has custom service logic.
For now, comment it out, and replace it with the generic code.
We will talk about this in the customization section.
Once all the services have been refactored, run yarn develop
to see if we get any more errors.
Another error. At least it is different.
[2022-07-26 19:20:59.256] error: GraphQL Nexus: Enum ENUM_RESTAURANT_PRICE must have at least one member
Error: GraphQL Nexus: Enum ENUM_RESTAURANT_PRICE must have at least one member
We will fix this in just a moment.
You will undoubtedly run into codebase-specific errors when working on your migration; this is normal, and you will have to debug.
As in our case, now we have an issue in our schema.json
file located in api/restaurant/content-types/Restaurant/schema.json
.
We have an ENUM that starts with a number, which is not allowed and has to be fixed.
Let's make the following change from this.
1 "price": {
2 "type": "enumeration",
3 "enum": [
4 "_1",
5 "_2",
6 "_3",
7 "_4"
8 ]
9 },
10 "district": {
11 "type": "enumeration",
12 "enum": [
13 "_1st",
14 "_2nd",
15 "_3rd",
16 "_4th",
17 "_5th",
18 "_6th",
19 "_7th",
20 "_8th",
21 "_9th",
22 "_10th",
23 "_11th",
24 "_12th",
25 "_13th",
26 "_14th",
27 "_15th",
28 "_16th",
29 "_17th",
30 "_18th",
31 "_19th",
32 "_20th"
33 ]
34 },
To this.
1 "price": {
2 "type": "enumeration",
3 "enum": [
4 "one",
5 "two",
6 "three",
7 "four"
8 ]
9 },
10
11 "district": {
12 "type": "enumeration",
13 "enum": [
14 "first",
15 "second",
16 "third",
17 "fourth",
18 "fifth",
19 "sixth",
20 "seventh",
21 "eighth",
22 "ninth",
23 "tenth",
24 "eleventh",
25 "twelfth",
26 "thirteenth",
27 "fourteenth",
28 "fifteenth",
29 "sixteenth",
30 "seventeenth",
31 "eighteenth",
32 "nineteenth",
33 "twentieth"
34 ]
35 },
The above should fix our error. But before we run yarn develop
, there is one more file that we have to add.
The dedicated bootstrap.js
file no longer exists in Strapi v4 and is now a global function combined with the new register
function. bootstrap()
and register()
can be found in ./src/index.js
Inside our src
folder, let's create an index.js
file and add the following code.
1"use strict";
2
3module.exports = {
4 /**
5 * An asynchronous register function that runs before
6 * your application is initialized.
7 *
8 * This gives you an opportunity to extend code.
9 */
10
11 register(/*{ strapi }*/) {},
12
13 /**
14 * An asynchronous bootstrap function that runs before
15 * your application gets started.
16 *
17 * This gives you an opportunity to set up your data model,
18 * run jobs, or perform some special logic.
19 */
20
21 bootstrap(/*{ strapi }*/) {},
22};
You can learn more about it here.
Let's run yarn develop
and see what happens.
Great success. Our app builds.
You can now create an admin user and log in.
We still need to go through our codebase and manually update any of our custom routes, controllers, services, and any other customizations you may have.
The good news is that we are running Strapi v4, and although we can continue to reference the migration guide found here.
We can use strapi v4 documentation from this point on, which you can find here.
As you noticed in our Food Advisor demo application, we have a lot of customizations, including routes, policies, controllers, services, and more.
To go over all these items in detail will take a long time. But, the good news is it's already in our Strapi v4 documentation.
This post will show how to implement a custom controller and GraphQl resolver and where to go for help in the documentation when you are stuck.
We also have our Discord community, with a channel dedicated to migration from Strapi v3 to v4.
It's a great place to ask questions and work with others. If you are not yet a member, I highly recommend it.
You can join or Discord Community here
But the goal is not to show everything but to empower you to feel comfortable using our documentation and Discord community to find solutions on your own.
We feel this is the best approach since it promotes learning and sharing. That said, we will release additional video resources covering these topics in greater detail.
Let's take a look at how we can create a custom controller. The process here will outline how I use the documentation to help me accomplish my goals.
We will create a basic example, which you can customize based on your needs.
In our code, let's look at the Restaurant.js
file you can find it in our controller's folder in api/restaurant/controllers
.
As you can see, we commented on the Strapi v3 custom code. We will have to update this custom controller manually.
1// path: ./src/api/restaurant/controllers/restaurant.js
2
3const { createCoreController } = require("@strapi/strapi").factories;
4
5module.exports = createCoreController("api::restaurant.restaurant");
6
7// will come back to this later
8
9// module.exports = {
10// find: async (ctx) => {
11// let restaurants;
12
13// if (ctx.query._q) {
14// restaurants = await strapi.api.restaurant.services.restaurant.search(ctx.query);
15// } else {
16// restaurants = await strapi.api.restaurant.services.restaurant.find(ctx.query);
17// }
18
19// restaurants = await Promise.all(
20// restaurants.map(async (restaurant) => {
21// restaurant.note = await strapi.api.review.services.review.average(restaurant.id);
22
23// return restaurant;
24// })
25// );
26
27// return restaurants;
28// },
29
30// findOne: async (ctx) => {
31// const { id } = ctx.params;
32// let restaurant = await strapi.api.restaurant.services.restaurant.findOne({ id });
33
34// if (!restaurant) {
35// return ctx.notFound();
36// }
37
38// restaurant.note = await strapi.api.review.services.review.average(restaurant.id);
39
40// let noteDetails = await strapi
41// .query('review')
42// .model.query(function (qb) {
43// qb.where('restaurant', '=', restaurant.id);
44// qb.groupBy('note');
45// qb.select('note');
46// qb.count();
47// })
48// .fetchAll()
49// .then((res) => res.toJSON());
50
51// restaurant.noteDetails = [];
52
53// for (let i = 5; i > 0; i--) {
54// let detail = noteDetails.find((detail) => {
55// return detail.note === i;
56// });
57
58// if (detail) {
59// detail = {
60// note: detail.note,
61// count: detail['count(*)'],
62// };
63// } else {
64// detail = {
65// note: i,
66// count: 0,
67// };
68// }
69
70// restaurant.noteDetails.push(detail);
71// }
72
73// return restaurant;
74// }
75// };
In the migration guide under the controller's section, we can take a look and see the difference between v3 and v4.
🤓 v3/v4 comparison
In both Strapi v3 and v4, creating content-types automatically generates core API controllers.
Controllers are JavaScript files that contain a list of methods, called actions.
In Strapi v3, controllers export an object containing actions that are merged with the existing actions of core API controllers, allowing customization.
In Strapi v4, controllers export the result of a call to the createCoreController
factory function, with or without further customization.
We can see an example of how we can implement the controller without customizations.
1// path: ./src/api/<content-type-name>/controllers/<controller-name>.js
2
3const { createCoreController } = require("@strapi/strapi").factories;
4
5module.exports = createCoreController("api::api-name.content-type-name");
It is something that we have already done.
Followed by an example of a custom controller. Something that we are going to do now.
1// path: ./src/api/<content-type-name>/controllers/<controller-name>.js
2
3const { createCoreController } = require("@strapi/strapi").factories;
4
5module.exports = createCoreController(
6 "api::api-name.content-type-name",
7 ({ strapi }) => ({
8 // wrap a core action, leaving core logic in place
9 async find(ctx) {
10 // some custom logic here
11 ctx.query = { ...ctx.query, local: "en" };
12
13 // calling the default core action with super
14 const { data, meta } = await super.find(ctx);
15
16 // some more custom logic
17 meta.date = Date.now();
18
19 return { data, meta };
20 },
21 })
22);
You can also learn more about custom controllers in our Strapi v4 documentation here.
Here they show three ways you can customize your controller.
1// path: ./src/api/restaurant/controllers/restaurant.js
2
3const { createCoreController } = require("@strapi/strapi").factories;
4
5module.exports = createCoreController(
6 "api::restaurant.restaurant",
7 ({ strapi }) => ({
8 // Method 1: Creating an entirely custom action
9 async exampleAction(ctx) {
10 try {
11 ctx.body = "ok";
12 } catch (err) {
13 ctx.body = err;
14 }
15 },
16
17 // Method 2: Wrapping a core action (leaves core logic in place)
18 async find(ctx) {
19 // some custom logic here
20 ctx.query = { ...ctx.query, local: "en" };
21
22 // Calling the default core action
23 const { data, meta } = await super.find(ctx);
24
25 // some more custom logic
26 meta.date = Date.now();
27
28 return { data, meta };
29 },
30
31 // Method 3: Replacing a core action
32 async findOne(ctx) {
33 const { id } = ctx.params;
34 const { query } = ctx;
35
36 const entity = await strapi
37 .service("api::restaurant.restaurant")
38 .findOne(id, query);
39 const sanitizedEntity = await this.sanitizeOutput(entity, ctx);
40
41 return this.transformResponse(sanitizedEntity);
42 },
43 })
44);
Method 1: Creating an entirely custom action Method 2: Wrapping a core action (leaves core logic in place) Method 3 Replacing a core action
We will use the example from the migration guide above, but I just wanted to show you where you can find resources.
Let's customize our controller with the following code.
1// path: ./src/api/restaurant/controllers/restaurant.js
2
3const { createCoreController } = require("@strapi/strapi").factories;
4
5module.exports = createCoreController(
6 "api::restaurant.restaurant",
7 ({ strapi }) => ({
8 async find(ctx) {
9 // some custom logic here
10 ctx.query = { ...ctx.query, local: "en" };
11
12 // calling the default core action with super
13 const { data, meta } = await super.find(ctx);
14
15 // some more custom logic
16 meta.date = Date.now();
17
18 console.log(meta);
19 return { data, meta };
20 },
21 })
22);
In the code above, we are creating a simple custom controller that adds the current date to our meta
object.
To test it out, first, we must enable the controller in our Strapi App.
You can do that by going into settings > roles > public > restaurant and checking the find endpoint.
Using Insomnia to make a GET request to our custom controller, we can see that we are now returning our date field in our meta
response object.
Using Insomnia, we make a GET request to our custom controller. We can see that we are now returning our date field in our meta
response object.
Although this was a simple example, you now know how to implement custom controllers and where to find information in the docs.
If you wanted to set up a custom route, you can learn how to do that here.
Let's do a quick example to change our controller to have its own route, but first, let's rename it to findCustomRoute.
Since we renamed our controller, it no longer overrides our previous find controller. Instead, it will be a new controller for which we will create a custom route.
1// path: ./src/api/restaurant/controllers/restaurant.js
2
3const { createCoreController } = require("@strapi/strapi").factories;
4
5module.exports = createCoreController(
6 "api::restaurant.restaurant",
7 ({ strapi }) => ({
8 async findCustomRoute(ctx) {
9 // some custom logic here
10
11 ctxquery = { ...ctx.query, local: "en" };
12
13 // calling the default core action with super
14 const { data, meta } = await super.find(ctx);
15
16 // some more custom logic
17 meta.date = Date.now();
18
19 console.log(meta);
20 return { data, meta };
21 },
22 })
23);
Inside our routes
folder, let's create a file called custom-routes.js
. We are using the example from the docs here
Let's add the following code to create our custom route.
1module.exports = {
2 routes: [
3 {
4 method: "GET",
5 path: "/restaurants/with-meta-date",
6 handler: "restaurant.findCustomRoute",
7 },
8 ],
9};
Before testing our custom route, once again, we have to enable the endpoint within Strapi.
Let's test the new endpoint /api/restaurants/with-meta-date
in Insomnia.
As we can see, our custom route and controller are working.
The above example should give you a good start on how to migrate custom routes and controllers.
Next, let's see how we can create a custom resolver with GraphQl.
Finally, let's look at how we can create custom GraphQl resolvers.
But first, let's review what changed from Strapi v3 to v4.
🤓 v3/v4 comparison
In Strapi v3, GraphQL resolvers are either automatically bound to REST controllers (from the core API) or customized using the ./api/<api-name>/config/schema.graphql.js
files.
In Strapi v4, GraphQL dedicated core resolvers are automatically created for the basic CRUD operations for each API. Additional resolvers can be customized programmatically using GraphQL’s extension service, accessible using strapi.plugin(’graphql’).service(’extension’)
.
Migrating GraphQL resolvers to Strapi v4 requires:
./api/<api-name>/config/schema.graphql.js
files, to the register
method found in the ./src/index.js
file of Strapi v4strapi.plugin(’graphql’).service(’extension’)
.You can read more in the documentation here
For our example, we will create a custom GraphQL resolver based on the above documentation.
Let's take a look inside the src/index.js
file. We should see the following.
1"use strict";
2
3module.exports = {
4 /**
5 * An asynchronous register function that runs before
6 * your application is initialized.
7 *
8 * This gives you an opportunity to extend code.
9 */
10
11 register(/*{ strapi }*/) {},
12
13 /**
14 * An asynchronous bootstrap function that runs before
15 * your application gets started.
16 *
17 * This gives you an opportunity to set up your data model,
18 * run jobs, or perform some special logic.
19 */
20
21 bootstrap(/*{ strapi }*/) {},
22};
Let's add the following code inside the register
function.
1register({ strapi }) {
2 const extensionService = strapi.service("plugin::graphql.extension");
3},
This will allow us to extend the GraphQL plugin and add custom resolvers.
We will write our custom resolver in this file directly, but you don't have to as long as you reference it here.
Let's override our find resolver. Go ahead and update the code inside the register
function to the following.
1 register({ strapi }) {
2 const extensionService = strapi.service('plugin::graphql.extension');
3
4 extensionService.use(({ strapi }) => ({
5 typeDefs: `
6 type Query {
7 restaurants(
8 filters: RestaurantFiltersInput
9 pagination: PaginationArg = {}
10 sort: [String] = []
11 publicationState: PublicationState = LIVE
12 ): RestaurantEntityResponseCollection
13 }
14 `,
15 resolvers: {
16 Query: {
17 restaurants: {
18 resolve: async (parent, args, context) => {
19 const { toEntityResponseCollection } =
20 strapi.service('plugin::graphql.format').returnTypes;
21 const { transformArgs } = strapi.service('plugin::graphql.builders').utils;
22 const contentType = strapi.contentTypes['api::restaurant.restaurant'];
23
24 const transformedArgs = transformArgs(args, { contentType });
25
26 const data = await strapi.entityService.findMany(
27 'api::restaurant.restaurant',
28 transformedArgs
29 );
30
31 const response = toEntityResponseCollection(data, {
32 args: { transformedArgs, start: 0, limit: 10 },
33 resourceUID: contentType.uid,
34 });
35
36 console.log('##################', response, '##################');
37 return response;
38 },
39 },
40 },
41 },
42 }));
43 },
You can learn more about Strapi GraphQL here
Once you have made the following changes, restart the server with yarn develop
, navigate to http://localhost:1337/graphql
, and add the following query.
query {
restaurants {
data {
id
attributes {
name
description
address
}
}
}
}
We should probably add a restaurant entry first before we test it.
I added one restaurant entry so we can see the response when we call our custom resolver.
Once you run the query, you will see the following result.
We will also see our console.log statement from our resolver on line 46 to confirm that we are hitting our custom GraphQl resolver.
################## {
nodes: [
{
id: 1,
name: 'Cat Treats',
description: 'Restaurant for cats',
address: 'Cats World',
website: 'bestcattreats.com',
phone: '1234567890',
price: 'one',
district: 'first',
publish_at: '2022-07-07T05:00:00.000Z',
previous_: null,
author_: null,
createdAt: '2022-07-27T20:24:30.668Z',
updatedAt: '2022-07-27T20:24:32.056Z',
publishedAt: '2022-07-27T20:24:32.053Z'
}
],
info: {
args: { transformedArgs: [Object], start: 0, limit: 10 },
resourceUID: 'api::restaurant.restaurant'
}
} ##################
You now have the tools to feel more comfortable with the migration process.
This is not the definitive guide, but it is a start.
We are also making additional video resources to dive into more detail on specific topics around the migration process.
I hope this article and its accommodating stream on youtube have helped to demystify the migration process and where to find the resources.
Let's continue the discussion of Discord.
This was a long article, so thank you for your support and patience.
If you have any issues or questions, please feel free to connect with me on Discord inside the v3 to v4 migration channel.