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.
Internal and External API
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];
- Open the http://localhost:1337/todo URL in your browser. You should see a
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.
- Update the
./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.
- Rename the
./src/plugins/todo/server/controller/my-controller.js
file totask.js
. - Modify the import in the
./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};
- Finally, replace the content of the
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};
- Wait for your server to restart and open the http://localhost:1337/todo/count URL in your browser. You should see a
todo
message.
What is left to do is to get the number of tasks instead of just a message.
- Create some tasks in the admin for your function to return something else than 0.
We are going to use a service to get the number of tasks.
- Rename the
./src/plugins/todo/server/services/my-service.js
bytask.js
- Modify the import in the
./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};
- Update the content of the
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});
- Update the
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.
- Give it a try by browsing the http://localhost:1337/todo/count URL in your browser.
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];
:::
Routes structuration
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:
- The first thing to do, is to replace the content of your
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');
- You can replace all of this content with custom routes like this:
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};
- Then, you just need to export this router in the
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:
- content-api: It is external: The routes will be available from this endpoint:
/api/plugin-name/...
. It needs to be activated in the Users & Permissions plugin setting in the admin. - admin: It is internal: The routes will be available from this endpoint:
/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
Strapi object
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:
- List content-types:
strapi.contentTypes
- List components:
strapi.components
- List plugins:
strapi.plugins
- Get plugin data (services, controllers, config, content-types):
strapi.plugin("plugin-name")
- Get/Set/Check config:
strapi.config
- Get/Set/Delete store:
strapi.store
- etc...
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
Relations
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.
- First, we need to update the schema file of the task (
./server/content-types/task/schema.json
) content-type to include amorphToOne
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.
- Update the
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.
Managing settings with the store
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.
- Update the
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.
- Update the
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
: UsesgetSettings
servicesetSettings
: UsessetSettings
serviceUpdate 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.
- Browse the http://localhost:1337/todo/settings URL, you should have the following result:
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