Strapi Internals: Customizing the Backend [Part 1 - Models, Controllers & Routes]
Tags: javascript, strapi, headlesscms
Published: 2022-05-25 12:41:58


[Strapi works as a Headless CMS and provides a lot of functionality out of the box](http://strapi.io), allowing it to be used for any use case without any modifications to the code. This doesn't  stop Strapi from providing customization options and extendable code that allows developers to fine-tune Strapi’s internal working to suit a special use case. Let’s dive into the internals of Strapi and how we can customize the backend. 


## Goal
We’re going to be working with the Strapi backend and cover a few aspects of customizations to the Strapi backend. We’re touching on controllers, services, policies, webhooks and routes, and others.

> This article is based on the [Strapi internals, customizing the backend workshop](https://www.youtube.com/watch?v=kohbOP6Eh9I) video by Richard from StrapiConf 2022

Strapi runs an HTTP server based on [Koa, a back-end JavaScript framework.](https://koajs.com/)

## What is Koa?
Koa aims to be a smaller, more expressive, and more robust foundation for web applications and APIs. If you are not familiar with the Koa backend framework, you should [read the Koa's documentation introduction](http://koajs.com/#introduction).

Leveraging Koa, Strapi provides a customizable backend and according to the backend customization docs, each part of Strapi's backend can be customized:

- The [requests](https://docs.strapi.io/developer-docs/latest/development/backend-customization/requests-responses.html#requests) received by the Strapi server,
- The [routes](https://docs.strapi.io/developer-docs/latest/development/backend-customization/routes.html) that handle the requests and trigger the execution of their controller handlers,
- The [policies](https://docs.strapi.io/developer-docs/latest/development/backend-customization/policies.html) that can block access to a route,
- The [middlewares](https://docs.strapi.io/developer-docs/latest/development/backend-customization/middlewares.html) that can control the request flow and the request before moving forward,
- The [controllers](https://docs.strapi.io/developer-docs/latest/development/backend-customization/controllers.html) that execute code once a route has been reached,
- The [services](https://docs.strapi.io/developer-docs/latest/development/backend-customization/services.html) that are used to build custom logic reusable by controllers,
- the [models](https://docs.strapi.io/developer-docs/latest/development/backend-customization/models.html) that are a representation of the content data structure,
- The [responses](https://docs.strapi.io/developer-docs/latest/development/backend-customization/requests-responses.html#responses) sent to the application that sent the request, and
- The [webhooks](https://docs.strapi.io/developer-docs/latest/development/backend-customization/webhooks.html) that are used to notify other applications of events that occur.

We’ll be covering these parts of Strapi backend while building the custom functionality for our order confirmation API

## Use Case
The use case for this is very basic. We’re creating the backend for a shop where we have users that can make orders and can also confirm the orders.

To achieve our use case and be build custom functionalities that we need and Strapi does not provide, we’ll get our hands on the backend code and build out those functionalities.


## Prerequisites
- Basic JavaScript knowledge
- Node.js (I’ll be using v16.13.0)
- A code  editor, I’ll be using VScode, you can get it from [the official website](https://code.visualstudio.com/download).
- Prior Strapi knowledge is helpful, but not required.


## Setting up

Let’s set up a basic strapi application with the `--quickstart` option. This creates a strapi instance with a simple SQLite database. 

```bash
    yarn create strapi-app strapi-backend --quickstart 
    #OR
    npx create-strapi-app@latest strapi-backend --quickstart
```

> I’m using Strapi v4.1.9 which is the latest at the time of creating this project

After installing the Strapi app, run the following command.
```bash
    yarn develop
    #OR
    npm run develop
```

This should open up a new tab in the browser to `http://localhost:1337/admin`, which will redirect us to the registration page where we will create an admin user.


![Strapi registration page](https://paper-attachments.dropbox.com/s_1D52D011EBFD4E890CAAF74FC362E0CF2B5C3A427A05A0874DA463C09FEEDB3D_1652041449432_image.png)


We’ll enter our details and once this is done, hit the **“Let’s start”** button. A new admin account will be created and we’ll be redirected back to `http://localhost:1337/admin/`.


## Creating our Models

Now, let’s quickly create two content types: Products & Orders

1. "Product" should include the following fields:
    - `name` - Short Text
    - `product_code` - Short Text

Here’s what the content type should look like:

![Product content-type structure](https://paper-attachments.dropbox.com/s_1D52D011EBFD4E890CAAF74FC362E0CF2B5C3A427A05A0874DA463C09FEEDB3D_1652044366743_image.png)



2.  "Order" should include the following fields:
- `owner` - Relation (`one-way` relation with User from users-permissions)
        
![Order content-type - Owner relation field options](https://paper-attachments.dropbox.com/s_1D52D011EBFD4E890CAAF74FC362E0CF2B5C3A427A05A0874DA463C09FEEDB3D_1652044844949_image.png)
 -  `products`  Relation (`many-way`  relation with **Product** )
        
![Order content-type - products relation field type](https://paper-attachments.dropbox.com/s_1D52D011EBFD4E890CAAF74FC362E0CF2B5C3A427A05A0874DA463C09FEEDB3D_1652045129901_image.png)
-  `confirmed` - Boolean
- `confirmation_date` - Datetime

Here’s what the content type should look like:


![Order content-type structure](https://paper-attachments.dropbox.com/s_1D52D011EBFD4E890CAAF74FC362E0CF2B5C3A427A05A0874DA463C09FEEDB3D_1652044984475_image.png)


We just created content type models using the Content-Type builder in the admin panel. We could also create these content types using the `strapi generate` with [Strapi’s interactive CLI tool.](https://docs.strapi.io/developer-docs/latest/developer-resources/cli/CLI.html#strapi-generate)

The content-types has the following models files:

- `schema.json` for the model's [schema](https://docs.strapi.io/developer-docs/latest/development/backend-customization/models.html#model-schema) definition. (generated automatically when creating content-type with either method)
- `lifecycles.js` for [lifecycle hooks](https://docs.strapi.io/developer-docs/latest/development/backend-customization/models.html#lifecycle-hooks). This file must be created manually.

#### Product Content-Type Schema
We can check out the model schema definition for the Products in the `./src/api/product/content-types/product/schema.json` file in our Strapi project code.


```js    
    // ./src/api/product/content-types/product/schema.json
    {
      "kind": "collectionType",
      "collectionName": "products",
      "info": {
        "singularName": "product",
        "pluralName": "products",
        "displayName": "Product"
      },
      "options": {
        "draftAndPublish": true
      },
      "pluginOptions": {},
      "attributes": {
        "name": {
          "type": "string"
        },
        "product_code": {
          "type": "string"
        }
      }
    }
```

#### Order Content-Type Schema
The model schema definition for Order would also be in the `./src/api/order/content-types/order/schema.json` file.

```js
    // ./src/api/order/content-types/order/schema.json
    
    {
      "kind": "collectionType",
      "collectionName": "orders",
      "info": {
        "singularName": "order",
        "pluralName": "orders",
        "displayName": "Order",
        "description": ""
      },
      "options": {
        "draftAndPublish": true
      },
      "pluginOptions": {},
      "attributes": {
        "owner": {
          // define a relational field
          "type": "relation",
          "relation": "oneToOne",
          "target": "plugin::users-permissions.user"
        },
        "confirmed": {
          "type": "boolean"
        },
        "confirmation_date": {
          "type": "datetime"
        },
        "products": {
          "type": "relation",
          "relation": "oneToMany",
          "target": "api::product.product"
        }
      }
    }
```

Now that we’ve seen what the models look like in the backend code, let’s dive into what we're trying to build while exploring these customizations.


## What We’re Building

As we previously discussed, we’re trying to create a store API and currently Strapi automatically provides us with routes that perform basic CRUD operations and we can take a look at them if we go to **SETTINGS** in our admin dashboard and then **USERS & PERMISSIONS PLUGIN > ROLES > PUBLIC**. 


![Permissions showing default routes for Order content type](https://paper-attachments.dropbox.com/s_1D52D011EBFD4E890CAAF74FC362E0CF2B5C3A427A05A0874DA463C09FEEDB3D_1652159733021_image.png)


In the image above, we can see the default pre-defined routes that Strapi creates for our `Order` content type.

Now, we want to take it a step further and add another level of customization.  The feature that we’re going for is for users to be able to create orders and confirm those orders they’ve made.

A very basic way of achieving this would be by using the `update` route on the `Order` content type to modify the `confirmed` and `confirmation_date` fields. But in a lot of situations, we might need more than just that and that’s what we’ll be working on.

## Custom Controllers and Routes

The first thing that we’ll be doing is to make sure that we have controllers and routes set up, knowing that we want to be able to confirm our orders . 

[Controllers are a very important aspect of how Strapi works and play a big role in customizing the backend](https://docs.strapi.io/developer-docs/latest/development/backend-customization/controllers.html#controllers). So, let’s go ahead and create a blank controller and a route for it.

#### Create a Controller
To define a custom controller inside the core controller file for the `order` endpoint or collection type,  we can pass in a function to the `createCoreController` method which takes in an object as a parameter and destructuring it, we’ll pass in `strapi`.

```js
    // ./src/api/order/controllers/order.js
    'use strict';
    /**
     *  order controller
     */
    const { createCoreController } = require('@strapi/strapi').factories;
    
    module.exports = createCoreController('api::order.order', ({strapi}) => ({
      confirmOrder: async (ctx, next) => {
        ctx.body = "ok"
      }
    }));
```

Here, the function we passed to `createCoreController` returns an object where we can specify an async function `confimOrder`, which takes `ctx` and `next` as parameters. Within this function, we can define a response, `ctx.body = "ok"`.

That’s how we can create a custom controller within the core controller in the default `order` route file. For illustration, we can completely overwrite an already existing controller, like `find` for example:

```js
    // ./src/api/order/controllers/order.js
    
    ...
    module.exports = createCoreController('api::order.order', ({strapi}) => ({
      confirmOrder: async (ctx, next) => {
        ctx.body = "ok"
      },
      find: async (ctx, next) => {
        // destructure to get `data` and `meta` which strapi returns by default
        const {data, meta} = await super.find(ctx)
        
        // perform any other custom action
        return {data, meta}
      }
    }));
```

Here, we’ve completely overwritten the default `find` controller, although we’re still running the same find function using `super.find(ctx)`. Now, we can start to add the main logic behind our `confirmOrder` controller. 

Remember that we’re trying to create a controller that allows us to confirm orders. Here are a few things we need to know:

- What order will be confirmed, and 
- Which user is confirming the order.

To know what order is being confirmed, we’ll have to get the `id` of that order from the route, so the route `path` we’ll create later on is going to include a dynamic `:id` parameter. Which is what we’ll pull out from `ctx.request.params` in our controller.

```js
    // ./src/api/order/controllers/order.js
    
    module.exports = createCoreController('api::order.order', ({strapi}) => ({
      confirmOrder: async (ctx, next) => {
        const {id} = ctx.request.params
        console.log(id);
      },
    }));
```

The next thing we need to do is to create a route that will be able to run our controller.

#### Create a Route
We’re going to create custom route definitions for our `confirmOrder` controller. If we take a look at the already created `order.js` route, we’ll see that the core route has already been created:

```js
    // ./src/api/order/routes/order.js
    
    'use strict';
    /**
     * order router.
     */
    const { createCoreRouter } = require('@strapi/strapi').factories;
    module.exports = createCoreRouter('api::order.order'); // core route already created 
```

We don’t have to make any modifications here in order to create our custom routes; we can create a new file for that. In order to access the controller we just created from the API, we need to attach it to a route.

**Create a new file to contain our custom route definitions in the `order/routes` directory - `./src/api/order/routes/confirm-order.js`**

```js
    // ./src/api/order/routes/confirm-order.js
    
    module.exports = {
      routes: [
        {
          method: "POST",
          path: "/orders/confirm/:id",
          handler: "order.confirmOrder"
        }
      ]
    }
```

What we’re basically doing here is creating an object with a `routes` key, which has a value of an array of route objects. 

The first object here defines a route with the `method` of `POST` and a `path` - `/orders/confirm/:id`, where the `/:id` is a dynamic URL parameter and is going to change based on the `id` of the order we’re trying to confirm. 

It also defines the `handler`, which is the controller that will be used in the route and in our case, that would be the `confirmOrder` controller we created.


## Test the Custom Controllers and Routes

Let’s test our custom routes and controllers now shall we? Run:

```bash
    yarn develop
```

Once the app is running, we can start sending requests with any API tester of our choice. I’ll be using [Thunder Client.](https://www.thunderclient.com/) It’s a VSCode extension,  you can [download it from the marketplace.](https://marketplace.visualstudio.com/items?itemName=rangav.vscode-thunder-client)

Once, you’ve gotten your API tester set up, send a `POST` request to `http://localhost:1337/api/orders/confirm/1`.


![](https://paper-attachments.dropbox.com/s_1D52D011EBFD4E890CAAF74FC362E0CF2B5C3A427A05A0874DA463C09FEEDB3D_1652257276958_image.png)


As you can see, we’re getting a `403` forbidden error. That’s because Strapi doesn't return anything for unauthenticated routes by default. We need to modify the Permissions in Strapi in order for it to be available to the public.

To do that, go to the Strapi admin dashboard, then go to **SETTINGS** in our admin dashboard and then **USERS & PERMISSIONS PLUGIN > ROLES > PUBLIC**.


![Enable confimOrder action in permissions](https://paper-attachments.dropbox.com/s_1D52D011EBFD4E890CAAF74FC362E0CF2B5C3A427A05A0874DA463C09FEEDB3D_1652257698727_image.png)


As you can see, we have a new action - `confirmOrder`. Enable it and click on **SAVE**. Now, if we try to send the request again, you should see the screenshot below.


![Request response and server logs](https://paper-attachments.dropbox.com/s_1D52D011EBFD4E890CAAF74FC362E0CF2B5C3A427A05A0874DA463C09FEEDB3D_1652258027275_image.png)


On our server, we can see that it logged the `id` as we defined in our controller. We’re now getting a `404` error, don’t worry, a different error is progress. We’re getting a `NotFoundError` because we never returned any response in out `confirmOrder` controller, we only did a `console.log`. Now that we’ve seen that it works, let’s build the main functionality.


## Building the Logic for the "confirmOrder" Controller

Remember there are a few things we need to know:

- What order is going to be confirmed - from the request order `id`
- What user is confirming the order - from the  context state

#### Getting the Order  `id`
In the controller, let’s return the `id` instead of simply logging it:

```js
    // ./src/api/order/controllers/order.js
      confirmOrder: async (ctx, next) => {
        const {id} = ctx.request.params
        return id
      },
```
Send the request again:


![API request response returning order id](https://paper-attachments.dropbox.com/s_1D52D011EBFD4E890CAAF74FC362E0CF2B5C3A427A05A0874DA463C09FEEDB3D_1652258539250_image.png)


Great! That works. We’ve been able to get the order `id`, let’s move further to get the user sending the request.

#### Getting the User
In the `confimOrder` controller, we can get the authenticated `user` from the context state - `ctx.state`

```js
    // ./src/api/order/controllers/order.js
    ...
      confirmOrder: async (ctx, next) => {
        const {id} = ctx.request.params
        console.log(ctx.state.user)
        return id
      },
```

Now, if we send this request, we’ll see that the server logs out `undefined`.


![Server logs out undefined](https://paper-attachments.dropbox.com/s_1D52D011EBFD4E890CAAF74FC362E0CF2B5C3A427A05A0874DA463C09FEEDB3D_1652259982505_image.png)


That’s because we’re sending a request without authentication. Let’s create a new user to send requests from. In the Strapi dashboard, go to the **CONTENT MANAGER > USER** and click on **CREATE NEW ENTRY** to create a new user. 


![Create new authenticated user](https://paper-attachments.dropbox.com/s_1D52D011EBFD4E890CAAF74FC362E0CF2B5C3A427A05A0874DA463C09FEEDB3D_1652269378723_image.png)


Make sure to set the role to **Authenticated**.

Next, we’re going send a login request with our newly created user details. In our API tester, send a `POST` request to the `http://localhost:1337/api/auth/local` endpoint and we’ll have all the details of that user including the JWT. 


![Login API request](https://paper-attachments.dropbox.com/s_1D52D011EBFD4E890CAAF74FC362E0CF2B5C3A427A05A0874DA463C09FEEDB3D_1652269763248_image.png)


We’ll go ahead and copy the token in the `jwt` field. We’ll need that to get our user in the confirm confirmation request. To do that, we’ll have to set Authorization headers in our API Tester.


![Authorization headers in API request](https://paper-attachments.dropbox.com/s_1D52D011EBFD4E890CAAF74FC362E0CF2B5C3A427A05A0874DA463C09FEEDB3D_1652270206088_image.png)


In the case of this extension, we can use the **Auth** options provided and place the token in the Bearer field.


![Bearer token Auth options in API request](https://paper-attachments.dropbox.com/s_1D52D011EBFD4E890CAAF74FC362E0CF2B5C3A427A05A0874DA463C09FEEDB3D_1652270291357_image.png)


Now, we’ll head over to the Strapi admin and set the permissions for Public and Authenticated users. In the Strapi admin dashboard, go to **SETTINGS** and then **USERS & PERMISSIONS PLUGIN > ROLES > PUBLIC.** Disable the `Order` actions and click the **Save** button. Next, go back to **ROLES** and select **AUTHENTICATED.**  Enable the actions for `Order`.


![Enabled Order permissions for Authenticated users](https://paper-attachments.dropbox.com/s_1D52D011EBFD4E890CAAF74FC362E0CF2B5C3A427A05A0874DA463C09FEEDB3D_1652309363225_image.png)


Once this is done, we’ll head back and send the request to `http://localhost:1337/api/orders/confirm/1` with the authorization headers.


![Confirm order API request with user info in terminal](https://paper-attachments.dropbox.com/s_1D52D011EBFD4E890CAAF74FC362E0CF2B5C3A427A05A0874DA463C09FEEDB3D_1652310190083_image.png)


Awesome! We see that all the user details are being logged out here on the console. 


> Make sure to never return sensitive user information contained in the user object  as an API response. Ensure you always sanitize your responses from sensitive data.

#### Getting the Order Data
Moving on, now that we have the order `id`  and are able to see who's confirming the order, we are going to get the order data by using Strapi’s `entityService`. Here’s an example of how we can use the `entityService` 

```js
    // ./src/api/order/controllers/order.js
    ...
      confirmOrder: async (ctx, next) => {
        const {id} = ctx.request.params
        const user = ctx.state.user
    
        // using the entityService to get content from strapi
        // entityService provides a few CRUD operations we can use
        // we'll be using findOne to get an order by id
        const order = await strapi.entityService.findOne("api::order.order", id)
        console.log(order)
        return id
      },
```

The `entityService.findOne()` takes in two parameters: 

- The `uid` of what we’re trying to find, which for the order is `api::order.order` 
- The parameters, which is the `id` of the order in this case

Save the changes, wait for the server to restart and then send another request to the confirm endpoint


![confirm order API request with order id response](https://paper-attachments.dropbox.com/s_1D52D011EBFD4E890CAAF74FC362E0CF2B5C3A427A05A0874DA463C09FEEDB3D_1652346905830_image.png)


So, it returns `null` which is okay since we don’t have any order created yet.
Next, we need to change the state of its confirmation and change the confirmation date

#### Update Order Data
To do that, we’ll use the `update`  method from `entityService` to update the `order`

```js
    // ./src/api/order/controllers/order.js
    ...
      confirmOrder: async (ctx, next) => {
        const { id } = ctx.request.params
        await strapi.entityService.update("api::order.order", id , {
          data: {
            confirmed: true,
            confirmation_date: new Date()
          }
        })
        return {
          message: "confirmed"
        }
      },
```

Here, you can see that we’re passing two things to the `update()`  method:

- The `uid` - `api::order.order` and
- The `id` of the `order` we want to update and 
- The `params` object which contains a `data` key with the value of an object where we set `confirmed` to `true` and assign a `confimation_date` with `new Date()`

Now that we’ve seen how we can update an order, remember that we don’t have any order created yet. Let’s work on that.

#### Create an Order
Before we go into that, if we take a look at the `order`  content type, we’ll see that it has an `owner` field. 


![Order content type structure showing Owner field](https://paper-attachments.dropbox.com/s_1D52D011EBFD4E890CAAF74FC362E0CF2B5C3A427A05A0874DA463C09FEEDB3D_1652350520753_image.png)


When creating a new order using the default `order` controller, the `owner` will have to be provided with the API request. That way, any user can send a request and still specify a different user in the `owner` field. That would be problematic. We don’t want that. 

What we can do instead is to modify the default controller so that the `owner` of the order can be inferred from the request context. Let’s enable the `create` action for orders in the **Authenticated Permissions settings**


![Enable create action for authenticated user](https://paper-attachments.dropbox.com/s_1D52D011EBFD4E890CAAF74FC362E0CF2B5C3A427A05A0874DA463C09FEEDB3D_1652354740756_image.png)


Hit **Save.** Now, we can go back to our code to customize the `create` controller

Let’s see how we can achieve that:

```js
    // ./src/api/order/controllers/order.js
    ...
      confirmOrder: async (ctx, next) => {
        ...
      },
      
      // customizing the create controller
      async create(ctx, next){
        // get user from context
        const user = ctx.state.user
        // get request body data from context
        const { products } = ctx.request.body.data
        console.log(products);
        // use the create method from Strapi enitityService
        const order = await strapi.entityService.create("api::order.order", {
          data: {
            products,
            // pass in the owner id to define the owner
            owner: user.id
          }
        })
        return { order }
      }
```

We have a few things going on here. We:

- Get the user from `ctx.state.user`, 
- Get the products from `ctx.request.body.data`
- Create a new order with `strapi.entityService.create()`, pass the `uid`  - `"api::order.order"`  and an object. The object we’re passing as parameters is similar to our request body but with the addition of the owner `id`.  
- Then, return the created order

To try out our  customized create order controller, we have to create a few products first. So, let’s head back to Strapi admin and navigate to  **CONTENT MANAGER > COLLECTION TYPES > PRODUCT > CREATE NEW ENTRY** and create a new product. 

Enter the name of the product and the product code and click on **SAVE** and then **PUBLISH.**


![Create new product](https://paper-attachments.dropbox.com/s_1D52D011EBFD4E890CAAF74FC362E0CF2B5C3A427A05A0874DA463C09FEEDB3D_1652357375235_image.png)


#### Create More Products


![Multiple products](https://paper-attachments.dropbox.com/s_1D52D011EBFD4E890CAAF74FC362E0CF2B5C3A427A05A0874DA463C09FEEDB3D_1652357474056_image.png)


Great! 

Now, let’s send a new `POST` request to the orders endpoint - `http://localhost:1337/api/orders` with authorization and the following body:

```js
    {
      "data": {
        "products": [
          2
        ]
      }
    }
```

We should see a new order created with the owner field populated.


![Create new order API request body and response](https://paper-attachments.dropbox.com/s_1D52D011EBFD4E890CAAF74FC362E0CF2B5C3A427A05A0874DA463C09FEEDB3D_1652360687323_image.png)


If we check the dashboard, we can see the new order:


![Newly created order](https://paper-attachments.dropbox.com/s_1D52D011EBFD4E890CAAF74FC362E0CF2B5C3A427A05A0874DA463C09FEEDB3D_1652362674359_image.png)


Great! 

#### Confirm an Order
Let’s try to confirm our newly created order and see what happens. 

![Confirm order request with confimed response](https://paper-attachments.dropbox.com/s_1D52D011EBFD4E890CAAF74FC362E0CF2B5C3A427A05A0874DA463C09FEEDB3D_1652367168378_image.png)


It works! If we check our Strapi dashboard, we should see it confirmed too.


![Confirmed order](https://paper-attachments.dropbox.com/s_1D52D011EBFD4E890CAAF74FC362E0CF2B5C3A427A05A0874DA463C09FEEDB3D_1652367313502_image.png)


## Conclusion 
We’ve been able to create custom routes and customize Strapi controllers, allowing us to perform custom actions, which we would not be able to with the default Strapi functionality. 

Currently, orders can be confirmed by just passing the order `id` to the request body. This means that any  (authenticated) user can pass that `id` in a request and confirm that order. We don’t want that. Although orders can only be created by an authenticated user, we only want the user who created the order to be able to confirm the order.



## Resources
In the [next part](https://strapi.io/blog/strapi-internals-customizing-the-backend-part-2-policies-webhooks) of this article, we’ll complete building out our order confirmation use case while exploring other customizations like Policies, utilities.

[The backend code for this part of the article can be accessed from here.](https://github.com/miracleonyenma/strapi-customizing-the-backend-strapi-backend/tree/part-1)