In part 1, we customized our controllers and routes to be able to create confirm orders, but we need to know that currently, orders can be confirmed by just passing the order id
to the request body.
Any (authenticated) user can pass that id
in a request and confirm that order. We don’t want that. Although an authenticated user can only create orders, we only wish to the user who created the order to be able to confirm the order.
This section will cover other aspects of customizing Strapi, particularly Policies. Let’s dive in.
Based on the Strapi docs, we know that policies are functions that execute a specific logic on each request before it reaches the controller. They are mostly used for securing business logic.
Each route of a Strapi project can be associated with an array of policies. For example, a policy named is-admin
can check that the request is sent by an admin user and restrict access to critical routes.
Policies can be global or scoped. Global policies can be associated with any route in the project. Scoped policies only apply to a specific API or plugin.
Make sure the server is not running, and run the following command to create a new policy:
yarn strapi generate
We’ll get asked a few questions where we define the policy, name and API:
1 ? Strapi Generators policy - Generate a policy for an API
2 ? Policy name is-owner
3 ? Where do you want to add this policy? Add policy to an existing API
4 ? Which API is this for? order
Once we’re done, we should have this:
When we go to ./src/api/order/policies/is-owner.js
, we see our newly created policy file with a basic boilerplate.
1 // ./src/api/order/policies/is-owner.js
2
3 'use strict';
4 /**
5 * `is-owner` policy.
6 */
7 module.exports = (policyContext, config, { strapi }) => {
8 // Add your own logic here.
9 strapi.log.info('In is-owner policy.');
10 const canDoSomething = true;
11 if (canDoSomething) {
12 return true;
13 }
14 return false;
15 };
Since we only added this policy to the order
API, it will be applied to that scope, depending on the scope you define. It could be application-wide or not, like in this case.
If you want to use a policy in multiple places, not just this API, you can make it a global approach and apply it in multiple places. In this case, we want it to be available inside the order
API.
Policies, according to the Strapi docs, are functions that execute specific logic on each request before it reaches the controller. They are mostly used for securing business logic.
For our use case, we want to stop a confirmation request if the user sending the request does not own the order that’s being confirmed. It acts as a guard against requests that do not meet a defined condition.
If you look at the boilerplate code of the policy we just created above, you can see that there’s an if
statement. That’s the basic working of a policy, if it passes that condition, proceed with the request; if it doesn't, don't proceed.
In order to apply this policy to our route, we have to add it to the confirm-order.js
route file at ./src/api/order/routes/confirm-order.js
1 // ./src/api/order/routes/confirm-order.js
2
3 module.exports = {
4 routes: [
5 {
6 method: "POST",
7 path: "/orders/confirm/:id",
8 handler: "order.confirmOrder",
9 config: {
10 policies: ["api::order.is-owner"]
11 }
12 }
13 ]
14 }
id
from the policyContext
Let’s go back to our policy file at ./src/api/order/policies/is-owner.js
1 // ./src/api/order/policies/is-owner.js
2
3 'use strict';
4 /**
5 * `is-owner` policy.
6 */
7 const { id } = policyContext.request.params
8 const user = policyContext.state.user
9 const order = await strapi.entityService.findOne("api::order.order", id, {
10 populate: ["owner"]
11 })
12
13 console.log({
14 order_id: id,
15 order,
16 user_id: user.id
17 });
18
19 return false;
20 };
We get the order id
and user information from the request parameters in policyContext
and we get the order
using the findOne()
method provided by strapi.entityService
.
Remember that owner
is a relational field in the order
collection type and to get the owner
field data from the order
object, we have to populate it. We did this by passing the populate
parameter
1 const order = await strapi.entityService.findOne("api::order.order", id, {
2 populate: ["owner"]
3 })
Let’s log the data to the console to see how it works. When we send a confirmation order request, we should get this:
We successfully got the order
, the owner
and user
information in the console and a policy error as a response, which is fine since we returned false
from our policy.
Note that if we make too many requests using the
entityService
for example, we can slow our application down. What we can do is abstract all that responsibility to the services that we call from our controllers
Next, we will check whether the user sending the request is the owner of the order.
Now that we have access to the user data and order data from policyContext
, let’s write a condition to check if the user is the owner of the order.
1 // ./src/api/order/policies/is-owner.js
2 ...
3 if(order.owner.id === user.id){
4 // Go ahead to excecute
5 return true;
6 }
7 // Error thrown when condition not met
8 throw Error
This checks if the user sending the request owns the order. If the id
s match, the policy returns true and proceeds with the request. If they don't, it throws an error.
We can modify the default policy error message to include more details. To do that, we need to pull put a few tools from Strapi utils.
1 // ./src/api/order/policies/is-owner.js
2
3 'use strict';
4 const utils = require("@strapi/utils")
5 const {PolicyError} = utils.errors
6
7 module.exports = async (policyContext, config, { strapi }) => {
8
9 const { id } = policyContext.request.params
10 const user = policyContext.state.user
11 const order = await strapi.entityService.findOne("api::order.order", id, {
12 populate: ["owner"]
13 })
14
15 if(order.owner.id === user.id){
16 // Go ahead to excecute
17 return true;
18 }
19 // throw policy error
20 throw new PolicyError("Thou shall not pass!")
21 };
In the code above, we require Strapi utils
and get PolicyError
by destructuring utils.errors
. Then, in our if
statement, we check that the owner
and user
are equal. If they are, we return true
allowing the request to continue. If the condition is not met on the other hand, we throw the PolicyError
with the custom error message "Thou shall not pass!"
.
We can test the error by creating a new user, getting the JWT for that user, adding it to the request header and sending a confirmation request.
Head to the admin dashboard, go to CONTENT MANAGER > COLLECTION TYPES > USERS > CREATE NEW ENTRY and create a new user.
Next, head over to the API tester and send a login request to https://localhost:1337/api/auth/local
with the following body containing the email and password of the newly created user.
Then, we copy the JWT and replace the other user auth token with this new one. Send the confirmation request, and we should see that it returns our policy error.
Great. That works. Now if we replace the original auth token with the one for the first user and send the request:
We get confirmed
. Awesome! The policy works. Let’s dive further into a few other things we might want to do while customizing our application.
Before we get into creating webhooks, let's get a brief overview by seeing what the Strapi docs on webhooks says:
Webhook is a construct used by an application to notify other applications that an event has occurred. More precisely, a webhook is a user-defined HTTP callback. Using a webhook is an excellent way to tell third-party providers to start some processing (CI, build, deployment ...). A webhook works by delivering information to a receiving application through HTTP requests (typically POST requests).
While customizing our application, we might want to be able to integrate it with another application, and we can do that using webhooks. To illustrate this, let’s set up a very simple Koa application.
All we have to do is clone the project from Github and follow the instructions in the README.md
file to setup the project locally.
git clone https://github.com/miracleonyenma/strapi-customizing-backend-order-service
cd strapi-customizing-backend-order-service
npm install
Once we have the project locally, we’ll take a look at ./app.js
1 // ./app.js
2
3 const Koa = require("koa");
4 const logger = require("koa-logger")
5 const app = new Koa();
6 app.use(logger())
7 app.use(async (ctx) => {
8 console.log(ctx.request)
9 ctx.body = "ok"
10 });
11 app.listen(3000, () => {
12 console.log("Order service running on https://localhost:3000")
13 })
It will be listening for a request and logging out what it receives. Now let’s start our app.
npm start
#or
npm test #if you have nodemon installed
We should see that the app is running.
Let’s create a webhook in our Strapi dashboard. Go to SETTINGS > WEBHOOKS > CREATE NEW WEBHOOK. Then, we set the following:
url
- Points to our simple Koa app - https://localhost:3000
Click on SAVE. To test this webhook, click on TRIGGER. We get a successful response in our webhook, and our Koa application logs out the POST request sent by the webhook
We see here that the test trigger request was successful, and we have logged out the request data in the console. But this doesn't give us useful information and the request payload/body, to get that we need to add a body parser to our app so that we can get the request body our Strapi webhook sends.
First, install the package:
npm install koa-bodyparser
Next, we require the package and use in our app.
1 // ./app.js
2
3 const bodyParser = require("koa-bodyparser")
4 app.use(bodyParser())
With bodyParser
we can now get the response body - response.body
.
1 // ./app.js
2 ...
3 app.use(async (ctx) => {
4 console.log(ctx.request.body)
5 ctx.body = "ok"
6 });
7 ...
So, if we trigger the webhook again, we should get this:
Here, we see that the request sent was a trigger-test
. Now, if we perform a different action among the events we’ve enabled, say, create a new order, the webhook will trigger and send a request with a body of the action performed. To see it in action, let’s create a new order:
Send a POST request to https://localhost:1337/api/orders
with the following body:
1 {
2 "data": {
3 "products": [
4 2
5 ]
6 }
7 }
We should get this response and see the request.body
in the console with the description of the event and the entry
information.
That’s basically how webhooks work. Next, we’re going to take a look at another aspect of customizing Strapi, Middlewares.
Middlewares are an essential aspect of any backend application, and there are many use cases for middleware. In our use case, to illustrate how we can help our middleware in our application, we’ll create a middleware that limits the rate at which users create or confirm orders.
To get started we have to install a rate limit package:
yarn add koa2-ratelimit
Once that’s installed, we can generate a middleware. Run:
yarn strapi generate
Follow the steps to create a new middleware:
1 ? Strapi Generators middleware: - Generate a middleware for an API
2 ? Middleware name: ratelimit
3 ? Where do you want to add this middleware? Add middleware to an existing API
4 ? Which API is this for? order
We should end up with something like this:
If we check, we’ll see that a new file has been created at ./api/order/middlewares/ratelimit.js
with the basic middleware boilerplate.
Replace the file content with this:
1 // ./api/order/middlewares/ratelimit.js
2
3 "use strict";
4 module.exports =
5 (config, { strapi }) =>
6 async (ctx, next) => {
7 const ratelimit = require("koa2-ratelimit").RateLimit;
8 const message = [
9 {
10 messages: [
11 {
12 id: "Auth.form.error.ratelimit",
13 message: "Too many attempts, please try again in a minute.",
14 },
15 ],
16 },
17 ];
18 return ratelimit.middleware(
19 Object.assign(
20 {},
21 {
22 interval: 1 * 60 * 1000,
23 max: 5,
24 prefixKey: `${ctx.request.path}:${ctx.request.ip}`,
25 message,
26 },
27 config
28 )
29 )(ctx, next);
30 };
Here’s an implementation of rate-limiting.
First we require the koa2-ratelimit
plugin package,
1 const ratelimit = require("koa2-ratelimit").RateLimit
Then, we specify the message
1 const message = [
2 {
3 messages: [
4 {
5 id: "Auth.form.error.ratelimit",
6 message: "Too many attempts; please try again in a minute.",
7 },
8 ],
9 },
10 ];
Finally, we returned the middleware. In this case, we produce an object with multiple configs for multiple points:
1 return ratelimit.middleware(
2 Object.assign(
3 {},
4 {
5 interval: 1 * 60 * 1000,
6 max: 5,
7 prefixKey: `${ctx.request.path}:${ctx.request.ip}`,
8 message,
9 },
10 config
11 )
12 )(ctx, next);
In the config above, we have:
interval
, which specifies the rate at which we want the requests to happen,max
, specifies the maximum requests within that interval,prefixKey
, this is going to be stored in Strapi core memory to keep track of who’s making the requests, andconfig
, this is an extra config that is compatible with the ratelimit plugin. You can read more about the configuration for the plugin on its Github docs.To attach this middleware to our request, we add it as a config in our confirm-order
route.
1 // ./src/api/order/routes/confirm-order.js
2
3 module.exports = {
4 routes: [
5 {
6 method: "POST",
7 path: "/orders/confirm/:id",
8 handler: "order.confirmOrder",
9 config: {
10 policies: ["api::order.is-owner"],
11 // attach the middleware
12 middlewares: ["api::order.ratelimit"]
13 }
14 }
15 ]
16 }
Now, if we try to send more than 5 requests in a minute, we get an error which tells us we have too many requests an we need to try again in a minute.
The last thing we need to cover is Services, which is an integral part of customizing Strapi.
According to the Strapi docs on services:
Services are a set of reusable functions. They are particularly useful to respect the "don’t repeat yourself" (DRY) programming concept and to simplify controllers logic.
We want our service to send an email to the user whenever an order is created. To create a custom service, the way we go about it is very similar to how we create controllers.
First we go to our core service file for our orders
API ./src/api/order/services/order.js
1 // ./src/api/order/services/order.js
2
3 'use strict';
4 /**
5 * order service.
6 */
7 const { createCoreService } = require('@strapi/strapi').factories;
8 module.exports = createCoreService('api::order.order', ({strapi}) => ({
9 async sendEmail(orderId, user){
10 await strapi.plugins['email'].services.email.send({
11 to: user.email,
12 subject: 'Your order has been confirmed',
13 text: 'Congrats!',
14 html: 'Congrats!',
15 });
16 }
17 }));
We extended the core service function by passing a function ({strapi}) => ()
, which returns an object with an async sendEmail()
function with orderId
and user
passed as parameters.
Within the sendEmail()
function, we call email.send()
method from the Strapi email plugin - strapi.plugins['email']
.
Then we pass in the email configuration:
1 {
2 to: user.email,
3 subject: 'Your order has been confirmed',
4 text: 'Congrats!',
5 html: 'Congrats!',
6 }
Once this is done, we can call this service from our order
controller. Open up ./src/api/order/controllers/order.js
to add the strapi.service
method to the confirmOrder
controller.
1 // ./src/api/order/controllers/order.js
2
3 ...
4 module.exports = createCoreController('api::order.order', ({ strapi }) => ({
5 confirmOrder: async (ctx, next) => {
6 const { id } = ctx.request.params
7 console.log(id);
8 await strapi.entityService.update("api::order.order", id, {
9 data: {
10 confirmed: true,
11 confirmation_date: new Date()
12 }
13 })
14
15 //send an email
16 strapi.service("api::order.order").sendEmail(
17 // order id
18 id,
19 // user
20 ctx.state.user)
21
22 return {
23 message: "confirmed"
24 }
25 },
Great! You can go through the Strapi documentation on services to see how to customize and setup services further.
We’ve created custom routes and customized Strapi controllers, routes, policies, middlewares, webhooks, utilities, and services, allowing us to perform custom actions, which we would not be able to do with the default Strapi functionality.
This article is based on the Strapi internals, customizing the backend workshop video by Richard from StrapiConf 2022. It only covers a few customization aspects using a relatively simple use case.
At least, with this, the Strapi documentation will be more approachable and appreciated as it covers these topics in depth with extensive examples.
strapi-customizing-the-backend-strapi-backend
github repo, and