Skip to main content

Examples cookbook: Custom global middlewares

πŸ— Work in progress

The content of this page might not be fully up-to-date with Strapi 5 yet.

Prerequisites

This page is part of the back end customization examples cookbook. Please ensure you've read its introduction.

Out of the box, FoodAdvisor does not provide any custom middlewares that could use incoming requests and perform some additional logic before executing the controller code.

There are 2 types of middlewares in Strapi: route middlewares control access to a route while global middlewares have a wider scope (see reference documentation for middlewares customization).

Custom route middlewares could be used instead of policies to control access to an endpoint (see policies cookbook) and could modify the context before passing it down to further core elements of the Strapi server. This page will not cover custom route middlewares but rather illustrate a more elaborated usage for custom global middlewares.

Populating an analytics dashboard in Google Sheets with a custom middleware​

πŸ’­ Context:

In essence, a middleware gets executed between a request arriving at the server and the controller function getting executed. So, for instance, a middleware is a good place to perform some analytics.

Let’s create a rudimentary example of an analytics dashboard made with Google Spreadsheets to have some insights on which restaurants pages of FoodAdvisor are more visited.

Visiting a restaurant page updates the Google Sheets spreadsheet
Every GET request to a restaurant's page executes the code of a custom middleware, updating a Google Sheets spreadsheet in real-time.

🎯 Goals:

  • Create some utility functions that interact with Google Sheets.
  • Create a custom Strapi middleware that will create and/or update an existing Google Sheet document every time we have an incoming request to a Restaurants page of the FoodAdvisor project.
  • Append the custom middleware to the route where we want it to get executed.
πŸ€“ Related concept

Additional information can be found in the middlewares customization documentation.

πŸ§‘β€πŸ’» Code example:

  1. In the /api folder of the FoodAdvisor project, create a /restaurant/middlewares/utils.js file with the following example code:
Example utility functions that could be used to read, write and update a Google spreadsheet:

The following code allows reading, writing, and updating a Google spreadsheet given an API Key read from a JSON file and a spreadsheet ID retrieved from the URL:

Google Spreadsheet URL

Additional information can be found in the official Google Sheets API documentation.

src/api/restaurant/middlewares/utils.js

const { google } = require('googleapis');

const createGoogleSheetClient = async ({
keyFile,
sheetId,
tabName,
range,
}) => {
async function getGoogleSheetClient() {
const auth = new google.auth.GoogleAuth({
keyFile,
scopes: ['https://www.googleapis.com/auth/spreadsheets'],
});
const authClient = await auth.getClient();
return google.sheets({
version: 'v4',
auth: authClient,
});
}

const googleSheetClient = await getGoogleSheetClient();

const writeGoogleSheet = async (data) => {
googleSheetClient.spreadsheets.values.append({
spreadsheetId: sheetId,
range: `${tabName}!${range}`,
valueInputOption: 'USER_ENTERED',
insertDataOption: 'INSERT_ROWS',
resource: {
majorDimension: 'ROWS',
values: data,
},
});
};

const updateoogleSheet = async (cell, data) => {
googleSheetClient.spreadsheets.values.update({
spreadsheetId: sheetId,
range: `${tabName}!${cell}`,
valueInputOption: 'USER_ENTERED',
resource: {
majorDimension: 'ROWS',
values: data,
},
});
};

const readGoogleSheet = async () => {
const res = await googleSheetClient.spreadsheets.values.get({
spreadsheetId: sheetId,
range: `${tabName}!${range}`,
});

return res.data.values;
};

return {
writeGoogleSheet,
updateoogleSheet,
readGoogleSheet,
};
};

module.exports = {
createGoogleSheetClient,
};
  1. In the /api folder of the FoodAdvisor project, create a custom analytics middleware with the following code:
src/api/restaurant/middlewares/analytics.js

'use strict';

const { createGoogleSheetClient } = require('./utils');

const serviceAccountKeyFile = './gs-keys.json';
// Replace the sheetId value with the corresponding id found in your own URL
const sheetId = '1P7Oeh84c18NlHp1Zy-5kXD8zgpoA1WmvYL62T4GWpfk';
const tabName = 'Restaurants';
const range = 'A2:C';

const VIEWS_CELL = 'C';

const transformGSheetToObject = (response) =>
response.reduce(
(acc, restaurant) => ({
...acc,
[restaurant[0]]: {
id: restaurant[0],
name: restaurant[1],
views: restaurant[2],
cellNum: Object.keys(acc).length + 2 // + 2 because we need to consider the header and that the initial length is 0, so our first real row would be 2,
},
}),
{}
);

module.exports = (config, { strapi }) => {
return async (context, next) => {
// Generating google sheet client
const { readGoogleSheet, updateoogleSheet, writeGoogleSheet } =
await createGoogleSheetClient({
keyFile: serviceAccountKeyFile,
range,
sheetId,
tabName,
});

// Get the restaurant ID from the params in the URL
const restaurantId = context.params.id;
const restaurant = await strapi.entityService.findOne(
'api::restaurant.restaurant',
restaurantId
);

// Read the spreadsheet to get the current data
const restaurantAnalytics = await readGoogleSheet();

/**
* The returned data comes in the shape [1, "Mint Lounge", 23],
* and we need to transform it into an object: {id: 1, name: "Mint Lounge", views: 23, cellNum: 2}
*/
const requestedRestaurant =
transformGSheetToObject(restaurantAnalytics)[restaurantId];

if (requestedRestaurant) {
await updateoogleSheet(
`${VIEWS_CELL}${requestedRestaurant.cellNum}:${VIEWS_CELL}${requestedRestaurant.cellNum}`,
[[Number(requestedRestaurant.views) + 1]]
);
} else {
/** If we don't have the restaurant in the spreadsheet already,
* we create it with 1 view.
*/
const newRestaurant = [[restaurant.id, restaurant.name, 1]];
await writeGoogleSheet(newRestaurant);
}

// Call next to continue with the flow and get to the controller
await next();
};
};
  1. Configure the routes for the "Restaurants" content-type to execute the custom analytics middleware whenever a restaurant page is queried. To do so, use the following code:
src/api/restaurant/routes/restaurant.js

'use strict';

const { createCoreRouter } = require('@strapi/strapi').factories;

module.exports = createCoreRouter('api::restaurant.restaurant', {
config: {
findOne: {
auth: false,
policies: [],
middlewares: ['api::restaurant.analytics'],
},
},
});