Author: Kellen Bolger
Learn how to set the default "populate" options via route middleware. Instead of passing "populate" on each request from the frontend, the can handle this functionality via route-based middleware in the backend. This will allow you to keep your frontend requests lean and organized.
You can also use this to control what data will be returned by default and not allow the user to add additional populate options in the frontend.
What is Route Middleware?
In Strapi, Route Middleware has a more limited scope and is configured and used as middleware at the route level. You can learn more in the Strapi documentation. Now, let's jump in and learn how to set this up.
Sample Content Structure
In this example, I will use a simple blog post type consisting of a title, body, hero image, slug, and authors, which is a one-to-many relationship with a user. Each blog post can have many authors.
Before getting into how to implement custom middleware, let's look at why you might want to add middleware for this use case in the first place.
The Problem
By default, population structure needs to be defined and sent on each client-side
request, else the request will return only top-level parent content.
A GET
request to localhost:1337/api/blog-posts
returns the following:
// localhost:1337/api/blog-posts
{
"data": [
{
"id": 1,
"attributes": {
"title": "Test Blog Post",
"body": "Test blog content",
"slug": "test-blog-post",
"createdAt": "2022-08-09T18:45:19.012Z",
"updatedAt": "2022-08-09T18:45:21.710Z",
"publishedAt": "2022-08-09T18:45:21.707Z"
}
}
],
"meta": {
"pagination": {
"page": 1,
"pageSize": 25,
"pageCount": 1,
"total": 1
}
}
}
This is not ideal, seeing as important data has been excluded, such as the heroImage
and authors
' information.
Populate = *
An easy solution to the problem above involves adding populate=*
to the initial query.
localhost:1337/api/blog-posts?populate=*
returns the following:
// localhost:1337/api/blog-posts?populate=*
{
"data": [
{
"id": 1,
"attributes": {
"title": "Test Blog Post",
"body": "Test blog content",
"slug": "test-blog-post",
"createdAt": "2022-08-09T18:45:19.012Z",
"updatedAt": "2022-08-09T19:22:39.637Z",
"publishedAt": "2022-08-09T18:45:21.707Z",
"heroImage": {
"data": {
"id": 1,
"attributes": {
"name": "test_cat.jpeg",
"alternativeText": "test_cat.jpeg",
"caption": "test_cat.jpeg",
"width": 500,
"height": 500,
"formats": {
"thumbnail": {
"name": "thumbnail_test_cat.jpeg",
"hash": "thumbnail_test_cat_2bdaa9fbe9",
"ext": ".jpeg",
"mime": "image/jpeg",
"path": null,
"width": 156,
"height": 156,
"size": 5.01,
"url": "/uploads/thumbnail_test_cat_2bdaa9fbe9.jpeg"
}
},
"hash": "test_cat_2bdaa9fbe9",
"ext": ".jpeg",
"mime": "image/jpeg",
"size": 21.78,
"url": "/uploads/test_cat_2bdaa9fbe9.jpeg",
"previewUrl": null,
"provider": "local",
"provider_metadata": null,
"createdAt": "2022-08-09T19:06:25.220Z",
"updatedAt": "2022-08-09T19:06:25.220Z"
}
}
},
"authors": {
"data": {
"id": 1,
"attributes": {
"username": "testUser",
"email": "test@test.com",
"provider": "local",
"confirmed": true,
"blocked": false,
"createdAt": "2022-08-09T19:07:03.325Z",
"updatedAt": "2022-08-09T19:07:03.325Z"
}
}
}
}
}
],
"meta": {
"pagination": {
"page": 1,
"pageSize": 25,
"pageCount": 1,
"total": 1
}
}
}
While this does return more data, the main flaw with this approach is that you don't have control over what data is returned. You are still not receiving valuable information, such as the author's role
while also receiving data you might not care about.
Getting Granular
Instead of using populate=*
, you can filter the query using LHS Bracket syntax.
The query might look like this:
localhost:1337/api/blog-posts?populate[heroImage][fields]
[0]=name&populate[heroImage][fields]
[1]=alternativeText&populate[heroImage][fields]
[2]=caption&populate[heroImage][fields]
[3]=url&populate[authors][fields]
[0]=username&populate[authors][populate][role][fields]
[0]=name
While this correctly returns the data specified, it is not feasible to use. This query is quite unruly and certainly not something you'd want to consistently use throughout your application.
Enter... Query-String
Using query-string, we can implement the same query as above in a much more readable and reusable manner. The query can easily be used directly in the front-end of our application.
For example:
const qs = require('qs')
const query = qs.stringify(
{
populate: {
heroImage: {
fields: ['name', 'alternativeText', 'caption', 'url'],
},
authors: {
fields: ['username'],
populate: {
role: {
fields: ['name'],
},
},
},
},
},
{
encodeValuesOnly: true, // prettify URL
}
)
// `localhost:1337/api/blog-posts?${query}`
It successfully returns the same result as the above query where we used bracket syntax:
{
"data": [
{
"id": 1,
"attributes": {
"title": "Test Blog Post",
"body": "Test blog content",
"slug": "test-blog-post",
"createdAt": "2022-08-09T18:45:19.012Z",
"updatedAt": "2022-08-09T19:22:39.637Z",
"publishedAt": "2022-08-09T18:45:21.707Z",
"heroImage": {
"data": {
"id": 1,
"attributes": {
"name": "test_cat.jpeg",
"alternativeText": "test_cat.jpeg",
"caption": "test_cat.jpeg",
"url": "/uploads/test_cat_2bdaa9fbe9.jpeg"
}
}
},
"authors": {
"data": {
"id": 1,
"attributes": {
"username": "testUser"
}
}
}
}
}
],
"meta": {
"pagination": {
"page": 1,
"pageSize": 25,
"pageCount": 1,
"total": 1
}
}
}
For many use cases, this will be the logical end. However, if you find that you are re-using the same query over and over again, read on.
Query Logic in Middleware
Now that you know how to build useful queries, you can look at optimizing the process further by adding a query directly into route-based middleware.
Initializing the New Middleware
In Strapi, you can generate boilerplate code directly from the CLI. In your terminal, run the command:
yarn strapi generate
From there, navigate to middleware
.
You will be prompted to name the middleware. Then, you will need to select where you want to add this middleware.
For this example, choose Add middleware to an existing API
since you only want it to run on the blog-post route.
Now in the Strapi project, if you navigate to src > api > blog-post > middlewares > test.js
, you will see the following boilerplate:
'use strict'
/**
* `test` middleware.
*/
module.exports = (config, { strapi }) => {
// Add your own logic here.
return async (ctx, next) => {
strapi.log.info('In test middleware.')
await next()
}
}
Enable Middleware on Route
Before utilizing the middleware, you first need to enable it on the route.
If you head to
src > api > blog-post > routes > blog-post.js
, you'll see the default route configuration:
'use strict'
/**
* blog-post router.
*/
const { createCoreRouter } = require('@strapi/strapi').factories
module.exports = createCoreRouter('api::blog-post.blog-post')
Edit this file as follows:
'use strict'
/**
* blog-post router.
*/
const { createCoreRouter } = require('@strapi/strapi').factories
module.exports = createCoreRouter('api::blog-post.blog-post', {
config: {
find: {
middlewares: ['api::blog-post.test'],
},
},
})
Pro Tip: If you can't remember the internal UIDs of the middleware, which is
api::blog-post.test
, run the command below:
yarn strapi middlewares:list
This will give you a list of all internal middleware UIDs in your project
To see all of the available customizations for core routes, check out the docs.
Adding Logic to the Middleware
Now that thw middleware has been initialized in your project and added to the blog-post
route, it's time to add some logic.
The purpose of this middleware is so you do not need to build your query on the frontend to return the data you are looking to fetch.
By adding your logic directly to the middleware, all of the querying will happen automatically when you head to the localhost:1337/api/blog-post
route.
Instead of writing your query on the frontend, add it directly to the middleware, as such:
// src > api > blog-post > middlewares > test.js
module.exports = (config, { strapi }) => {
// This is where we add our middleware logic
return async (ctx, next) => {
ctx.query.populate = {
heroImage: {
fields: ['name', 'alternativeText', 'caption', 'url'],
},
authors: {
fields: ['username'],
populate: {
role: {
fields: ['name'],
},
},
},
}
await next()
}
}
Now, stop the Strapi server and run yarn strapi build
to rebuild your Strapi instance. Once the build is complete, run yarn develop
to restart the Strapi server.
If you go to the route localhost:1337/api/blog-posts
the response returns:
{
"data": [
{
"id": 1,
"attributes": {
"title": "Test Blog Post",
"body": "Test blog content",
"slug": "test-blog-post",
"createdAt": "2022-08-09T18:45:19.012Z",
"updatedAt": "2022-08-09T19:22:39.637Z",
"publishedAt": "2022-08-09T18:45:21.707Z",
"heroImage": {
"data": {
"id": 1,
"attributes": {
"name": "test_cat.jpeg",
"alternativeText": "test_cat.jpeg",
"caption": "test_cat.jpeg",
"url": "/uploads/test_cat_2bdaa9fbe9.jpeg"
}
}
},
"authors": {
"data": {
"id": 1,
"attributes": {
"username": "testUser"
}
}
}
}
}
],
"meta": {
"pagination": {
"page": 1,
"pageSize": 25,
"pageCount": 1,
"total": 1
}
}
}
No query string is needed!
Congrats on Making it to the End!
Here is a recap of what you've just learned:
- How to use
populate=*
. - How to query and filter using LHS Bracket syntax.
- How to use query-string to build custom client-side queries for better re-use.
- How to add custom middlewares to your project.
- How to implement middlewares on an api route.