This article is a continuation of the following content: Add a content-type to a plugin part 3/6
The server part of a plugin is nothing more than an API you can consume from the outside (http://localhost:1337/api/plugin-name/...) or in the admin of your plugin (http://localhost:1337/plugin-name/...). It is made of routes, controllers, and services but also middlewares and policies.
Knowing how to master this little API you are ready to create, is definitely important in the development of a plugin.
When creating an API, we first start by creating the route. We would like to be able to fetch the total number of tasks.
By default, Strapi generates the following route:
1// server/routes/index.js
2module.exports = [
3 {
4 method: 'GET',
5 path: '/',
6 handler: 'myController.index',
7 config: {
8 policies: [],
9 },
10 },
11];
This means that if you execute a GET request to the url http://localhost:1337/<name-of-your-plugin>
, the index
action of the myController
controller will be executed. In this case, the route is using authentication. We can make it public and see the result:
1// server/routes/index.js
2module.exports = [
3 {
4 method: 'GET',
5 path: '/',
6 handler: 'myController.index',
7 config: {
8 policies: [],
9 auth: false,
10 },
11 },
12];
Welcome to Strapi 🚀
message.Default routes, controllers, and services can be modified and this is what we are going to do. Again, the goal is to create a route for getting the total number of tasks.
./src/plugins/todo/server/routes/index.js
file with the following:1// server/routes/index.js
2module.exports = [
3 {
4 method: 'GET',
5 path: '/count',
6 handler: 'task.count',
7 config: {
8 policies: [],
9 auth: false,
10 },
11 },
12];
This route indicates that when requesting the URL: http://localhost:1337/todo/count
, the task controller will execute the count
action in order to return something.
./src/plugins/todo/server/controller/my-controller.js
file to task.js
../src/plugins/todo/server/controller/index.js
with the following:1// server/controller/index.js
2'use strict';
3
4const task = require('./task');
5
6module.exports = {
7 task,
8};
task.js
file with the following:1// server/controller/task.js
2'use strict';
3
4module.exports = {
5 count(ctx) {
6 ctx.body = 'todo';
7 },
8};
todo
message.What is left to do is to get the number of tasks instead of just a message.
We are going to use a service to get the number of tasks.
./src/plugins/todo/server/services/my-service.js
by task.js
./src/plugins/todo/server/services/index.js
with the following:1// server/services/index.js
2'use strict';
3
4const task = require('./task');
5
6module.exports = {
7 task,
8};
task.js
file with the following:1// server/services/task.js
2'use strict';
3
4module.exports = ({ strapi }) => ({
5 async count() {
6 return await strapi.query('plugin::todo.task').count();
7 },
8});
server/controllers/task.js
file with the following:1// server/controller/task.js
2'use strict';
3
4module.exports = {
5 async count(ctx) {
6 ctx.body = await strapi
7 .plugin('todo')
8 .service('task')
9 .count();
10 },
11};
If we summarize, the route tells your application that when receiving the http://localhost:1337/todo/count
GET request, the task controller will execute the count
action which will use the Query engine count function to return the actual count of tasks.
Tip: Do you remember when you created your tasks content-type using the CLI? We answered no at the last question which was Bootstrap API related files?
. If you said yes, Strapi would have generated the right controller, service, and route with correct and simple names so you don't have to modify it by yourself.
It is nice for you to see that you have the freedom to modify your files first but for your next content-type, you might want to answer yes to this question.
Just know that the default files would have been different. Let's see the controller file for example:
1// server/controllers/task.js
2'use strict';
3
4/**
5 * controller
6 */
7
8const { createCoreController } = require('@strapi/strapi').factories;
9
10module.exports = createCoreController('plugin::todo.task');
If you want to add an action to this controller as we did previously, you must do the following:
1// server/controllers/task.js
2'use strict';
3
4/**
5 * controller
6 */
7
8const { createCoreController } = require('@strapi/strapi').factories;
9
10module.exports = createCoreController('plugin::todo.task', {
11 async count(ctx) {
12 ctx.body = await strapi
13 .plugin('todo')
14 .service('task')
15 .count();
16 },
17});
It will be the same for services:
1// server/services/task.js
2'use strict';
3
4/**
5 * service.
6 */
7
8const { createCoreService } = require('@strapi/strapi').factories;
9
10module.exports = createCoreService('plugin::todo.task', {
11 async count() {
12 return await strapi.query('plugin::todo.task').count();
13 },
14});
Concerning the default router file, it is about the core router configuration. You can leave it like this and keep creating your routes in the server/routes/index.js
file:
1// server/routes/index.js
2module.exports = [
3 {
4 method: 'GET',
5 path: '/count',
6 handler: 'task.count',
7 config: {
8 policies: [],
9 auth: false,
10 },
11 },
12 {
13 method: 'GET',
14 path: '/findRandom',
15 handler: 'task.findRandomTask',
16 config: {
17 policies: [],
18 auth: false,
19 },
20 },
21];
:::
These endpoints will be accessible directly with this URL http://localhost:1337/plugin-name/<path>
without having permissions to set like you must do with content-api routes type. These ones are admin routes type.
We can better structure our routes:
server/routes/index.js
file with this:1module.exports = {};
Then, you can create a route file for every content-types you have. When saying Yes to the Bootstrap API related files?
question in the CLI, this is what Strapi does. It creates a server/routes/task.js
file with the following:
1// server/routes/task.js
2'use strict';
3
4/**
5 * router.
6 */
7
8const { createCoreRouter } = require('@strapi/strapi').factories;
9
10module.exports = createCoreRouter('plugin::todo.task');
1// server/routes/task.js
2'use strict';
3
4/**
5 * router.
6 */
7
8module.exports = {
9 type: 'admin', // other type available: content-api.
10 routes: [
11 {
12 method: 'GET',
13 path: '/count',
14 handler: 'task.count',
15 config: {
16 policies: [],
17 auth: false,
18 },
19 },
20 ],
21};
server/routes/index.js
:1// server/routes/index.js
2const task = require('./task');
3
4module.exports = {
5 task,
6};
If you have another content-type, then you just need to create another custom router: server/routes/report.js
and to export it:
1// server/routes/report.js
2'use strict';
3
4/**
5 * router.
6 */
7
8module.exports = {
9 type: 'content-api', // other type available: admin.
10 routes: [
11 {
12 method: 'GET',
13 path: '/',
14 handler: 'report.findMany',
15 config: {
16 policies: [],
17 auth: false,
18 },
19 },
20 ],
21};
1// server/routes/index.js
2const task = require('./task');
3const report = require('./report');
4
5module.exports = {
6 task,
7 report,
8};
Caution;Please be aware of the different types of routes:
/api/plugin-name/...
. It needs to be activated in the Users & Permissions plugin setting in the admin./plugin-name/...
and will only be accessible from the front-ent part of Strapi: the admin. No need to define permissions but you can enable or disable authentication.Learn more about routes in the documentation
In the previous section, we used the Query Engine to interact with the database layer.
1// server/services/task.js
2'use strict';
3
4/**
5 * service.
6 */
7
8const { createCoreService } = require('@strapi/strapi').factories;
9
10module.exports = createCoreService('plugin::todo.task', {
11 async count() {
12 return await strapi.query('plugin::todo.task').count(); // This
13 },
14});
It is important to know what the strapi
object allows you to do, and you can see this by using the strapi console
command:
# stop your server and run
yarn strapi console
# or
npm run strapi console
This will start your Strapi project and eval commands in your application in real-time. From there, you can type strapi
, press enter, and see everything you can have access to.
For example, you can:
strapi.contentTypes
strapi.components
strapi.plugins
strapi.plugin("plugin-name")
strapi.config
strapi.store
With strapi.store
, we get:
1[Function: store] {
2 get: [AsyncFunction: get],
3 set: [AsyncFunction: set],
4 delete: [AsyncFunction: delete]
5}
It means that strapi.store
has 3 async functions available for me to use in order to play with the application store. A global Strapi API reference existed for Strapi v3. It is outdated but some references are still working on v4.
Learn more about server customization in the documentation
Most of the time, when developing a plugin, you'll need to create a content-type. It can be independent by making the plugin work under the hood. In other scenarios, associating this plugin content-type to a regular (api) content-type (the ones you create in the admin), is possible and pretty easy to do.
For this guide, we want to have a to-do list for every content-type API our application contains. This means that we'll create a relation between the task content-type to every other content-type API.
However, we are not going to use the regular relations. In fact, for this use case, we'll use the specific relation that the Media Library is using for managing file relations: Polymorphic relationships. It is a good occasion to learn how to use them since they are not documented.
Tip: This relationship involves a column in a table (task table with an id) that can link to different columns in other tables (article table, product table, etc...). In a polymorphic relationship, a model/table can be associated with different models/tables.
This plugin will require a polymorphic relation to work properly. In fact, if you create an article
content-type and create a regular oneToMany relationship, it will work, your articles will have many related tasks, but if you create 99 other content-types, you'll need to create the 99 relationships manually in the admin...
Also, by creating a non-polymorphic oneToMany, manyToOne or manyToMany relation, Strapi will create a lookup database table to match your entries, this is how 'regular' relationships work. By creating a Polymorphic relation, only 1 table will be created whether you have 1 or 99 content-types related to your task content-type but this is true if you use a morphToMany relation for your task. If you use a morphToOne
, and this is the one we are going to use, no lookup table will be necessary!
In non-polymorphic relationships, the foreign keys reference a primary ID in a specific table. On the other hand, a foreign key in a polymorphic lookup table can reference many tables.
One other advantage of the polymorphic relation is that you'll don't have a right-links block in the content-manager displaying your tasks. We don't want that for our plugin since it will not be useful at all. We want to manage how we'll display our tasks in order to correctly interact with them
You can learn more by browsing the source code of Strapi. Here, you can find the schema file of the upload plugin (Media Library) that is using a polymorphic morphToMany
relation.
./server/content-types/task/schema.json
) content-type to include a morphToOne
relation.1// ./server/content-types/task/schema.json
2{
3 "kind": "collectionType",
4 "collectionName": "tasks",
5 "info": {
6 "singularName": "task",
7 "pluralName": "tasks",
8 "displayName": "Task"
9 },
10 "options": {
11 "draftAndPublish": false,
12 "comment": ""
13 },
14 "attributes": {
15 "name": {
16 "type": "string",
17 "required": true,
18 "maxLength": 40
19 },
20 "isDone": {
21 "type": "boolean",
22 "default": false
23 },
24 "related": {
25 "type": "relation",
26 "relation": "morphToOne"
27 }
28 }
29}
By selecting a morphToOne
related field, Strapi will create in the task table, a target_id
and a target_type
column. If you create a task for an article entry, you will fill the target_id
with the id of the article and the target_type
with the internal slug of the entry which will probably be: api::article.article
. But we'll see that later in the front-end section.
For a relationship to work, it must be indicated on both sides (1.task <> 2.article, product, page, etc...). We did half of the job. We are going to use the register phase of the plugin to automatically create the relation on every other content-types.
server/register.js
file with the following:1// server/register.js
2'use strict';
3
4module.exports = ({ strapi }) => {
5 // Iterating on every content-types
6 Object.values(strapi.contentTypes).forEach(contentType => {
7 // Add tasks property to the content-type
8 contentType.attributes.tasks = {
9 type: 'relation',
10 relation: 'morphMany',
11 target: 'plugin::todo.task', // internal slug of the target
12 morphBy: 'related', // field in the task schema that is used for the relation
13 private: false, // false: This will not be exposed in API call
14 configurable: false,
15 };
16 });
17};
This code will associate tasks to every content-types by creating a tasks
object containing the relation
type which will be a morphMany
here since you want this content-type to have multiple tasks using polymorphic relation.
However, even the other plugins will have this relation (i18n, Users and Permission, etc...). We can add a very simple condition to only associate the task content-type to content-type API:
1// server/register.js
2'use strict';
3
4module.exports = ({ strapi }) => {
5 // Iterating on every content-types
6 Object.values(strapi.contentTypes).forEach(contentType => {
7 // If this is an api content-type
8 if (contentType.uid.includes('api::')) {
9 // Add tasks property to the content-type
10 contentType.attributes.tasks = {
11 type: 'relation',
12 relation: 'morphMany',
13 target: 'plugin::todo.task', // internal slug of the target
14 morphBy: 'related', // field in the task schema that is used for the relation
15 private: false, // false: This will not be exposed in API call
16 configurable: false,
17 };
18 }
19 });
20};
In fact, every content-types created in the admin will have a uid beginning with api::
. For plugins, it will begin with plugin::
etc...
We created a polymorphic relation between a plugin content-type and every other content-type API.
A plugin might need to have some settings. This section will cover the server part of handling settings for a plugin. For this guide, we'll define a setting to disable or cross tasks when they are marked as done.
server/routes/task.js
file with the following:1// server/routes/task.js
2'use strict';
3
4/**
5 * router.
6 */
7
8module.exports = {
9 type: 'admin',
10 routes: [
11 {
12 method: 'GET',
13 path: '/count',
14 handler: 'task.count',
15 config: {
16 policies: [],
17 auth: false,
18 },
19 },
20 {
21 method: 'GET',
22 path: '/settings',
23 handler: 'task.getSettings',
24 config: {
25 policies: [],
26 auth: false,
27 },
28 },
29 {
30 method: 'POST',
31 path: '/settings',
32 handler: 'task.setSettings',
33 config: {
34 policies: [],
35 auth: false,
36 },
37 },
38 ],
39};
This custom router creates 2 new admin routes that will be using two new task
controller actions.
server/controllers/task.js
file with the following:1// server/controllers/task.js
2'use strict';
3
4/**
5 * controller
6 */
7
8const { createCoreController } = require('@strapi/strapi').factories;
9
10module.exports = createCoreController('plugin::todo.task', {
11 async count(ctx) {
12 ctx.body = await strapi
13 .plugin('todo')
14 .service('task')
15 .count();
16 },
17 async getSettings(ctx) {
18 try {
19 ctx.body = await strapi
20 .plugin('todo')
21 .service('task')
22 .getSettings();
23 } catch (err) {
24 ctx.throw(500, err);
25 }
26 },
27 async setSettings(ctx) {
28 const { body } = ctx.request;
29 try {
30 await strapi
31 .plugin('todo')
32 .service('task')
33 .setSettings(body);
34 ctx.body = await strapi
35 .plugin('todo')
36 .service('task')
37 .getSettings();
38 } catch (err) {
39 ctx.throw(500, err);
40 }
41 },
42});
This controller has two actions:
getSettings
: Uses getSettings
servicesetSettings
: Uses setSettings
service
Update the server/services/task.js
file with the following:
1// server/services/task.js
2'use strict';
3
4const { createCoreService } = require('@strapi/strapi').factories;
5
6function getPluginStore() {
7 return strapi.store({
8 environment: '',
9 type: 'plugin',
10 name: 'todo',
11 });
12}
13async function createDefaultConfig() {
14 const pluginStore = getPluginStore();
15 const value = {
16 disabled: false,
17 };
18 await pluginStore.set({ key: 'settings', value });
19 return pluginStore.get({ key: 'settings' });
20}
21
22module.exports = createCoreService('plugin::todo.task', {
23 async count() {
24 return await strapi.query('plugin::todo.task').count();
25 },
26 async getSettings() {
27 const pluginStore = getPluginStore();
28 let config = await pluginStore.get({ key: 'settings' });
29 if (!config) {
30 config = await createDefaultConfig();
31 }
32 return config;
33 },
34 async setSettings(settings) {
35 const value = settings;
36 const pluginStore = getPluginStore();
37 await pluginStore.set({ key: 'settings', value });
38 return pluginStore.get({ key: 'settings' });
39 },
40});
This service allows you to manage your plugin store. It will create a default config with an object containing a disabled
key to false. It means that, by default, we want our tasks to be crossed when marked as done not disabled. We'll see this in the next section.
1{
2 "disabled": false
3}
It is time for some admin customization.
Next article: Admin customization part 5/6
Maxime started to code in 2015 and quickly joined the Growth team of Strapi. He particularly likes to create useful content for the awesome Strapi community. Send him a meme on Twitter to make his day: @MaxCastres