Route-Based Middleware to Handle Default Population Query Logic

Route-Based Middleware to Handle Default Population Query Logic

Learn how to set the default "populate" options via route middleware in the backend.

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.

sample content structure

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.

middleware

You will be prompted to name the middleware. Then, you will need to select where you want to add this middleware.

run middleware

For this example, choose Add middleware to an existing API since you only want it to run on the blog-post route.

select 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

list of middleware

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.