✨ Strapi MCP is now Generally Available - let your agents manage your Strapi content ✨

Releases & Roadmap6 min read

How to Filter Results on Deeply-Nested fields in Strapi

April 11, 2022Updated on June 14, 2026
How to Filter Results in Strapi

Introduction

Strapi provides you with multiple ways to filter results on relation’s fields deeply. This article will cover the different ways to filter results using the following APIs:

  • REST API,
  • Entity Service API (Deprecated in Strapi 5),
  • Query Engine API,
  • GraphQL API, and
  • Document Service API.

Prerequisites

Before starting this content, you need:

  1. Basic knowledge of JavaScript
  2. A basic understanding of Strapi — get started here

Use Case

The following example will be used to demonstrate the deep filtering capabilities of Strapi:

A collection type called Book with the following fields:

  • title
  • authors Book collection with two fields- title and authors.png

Another collection type called Author with the following fields:

  • name
  • hobby
  • books

Author collection.png

Books can have multiple Authors, and Authors can write multiple Books and have different Hobbies.

Book and Author relation.png

The end goal is to filter Books based on their Authors' Hobbies. Some sample data will help us explore different approaches practically. Consider four authors:

IDNAMEHOBBY
1Author1play
2Author2sing
3Author3dance
4Author4dance

Author 1's hobby is to play, Author 2's is to sing, and Authors 3 and 4's hobby is to dance.

Consider the following books:

IDTITLEAUTHORS
1How to PlayAuthor1
2How to SingAuthor2
3How to DanceAuthor3
4How to be FlexibleAuthor4

A practical use case would be finding all books written by authors whose hobby contains the word 'dance', which we will explore in this article.

Deep Filtering Using the REST API

The following API request returns all books written by authors whose hobby contains the word 'dance':

Sample Request:

GET /api/books?filters[authors][hobby][$contains]=dance 

Sample Response:

{
    "data": [
        {
            "id": 10,
            "documentId": "kte9yxf6oami3680br3gapfl",
            "title": "How to be Flexible",
            "createdAt": "2025-01-16T05:49:53.037Z",
            "updatedAt": "2025-01-16T06:06:43.838Z",
            "publishedAt": "2025-01-16T06:06:43.851Z"
        },
        {
            "id": 6,
            "documentId": "mfnvnveobtf4fzporv9k2z3x",
            "title": "How to Dance",
            "createdAt": "2025-01-16T05:49:27.517Z",
            "updatedAt": "2025-01-16T05:49:27.517Z",
            "publishedAt": "2025-01-16T05:49:27.522Z"
        }
    ],
    "meta": {
        "pagination": {
            "page": 1,
            "pageSize": 25,
            "pageCount": 1,
            "total": 2
        }
    }
}

Queries accept a filters parameter with the following syntax:

GET /api/:pluralApiId?filters[field][operator]=value

Strapi supports a wide range of query operators. A comprehensive list of query operators can be found in the Strapi documentation.

Let's examine the example request:

GET /api/books?filters[authors][hobby][$contains]=dance
  • The /api/books endpoint without query parameters returns all books, subject to the default limit
  • The ?filters parameter enables result filtering
  • [authors] represents a relation field in the Book collection type
  • [hobby] represents a field in the Author collection type
  • [$contains] specifies the operator that Strapi supports
  • =dance specifies the search value

This query can be constructed programmatically in the frontend:

const qs = require('qs');
const query = qs.stringify({
  filters: {
    authors: {
      hobby: {
        $contains: dance,
      },
    },
  },
}, {
  encodeValuesOnly: true,
});

await request(`/api/restaurants?${query}`);
// GET /api/restaurants?filters[authors][hobby][$contains]=dance

Deep Filtering Using the Query Engine API

The Query Engine API allows users to filter results using its findMany() method.

The filters parameter is used to filter results. It supports both logical operators and attribute operators. Each operator must be prefixed with $.

The query equivalent for GET /api/books?filters[authors][hobby][$contains]=dance is as follows:

module.exports = {
  async index(ctx, next) {
    const entries = await strapi.db.query('api::book.book').findMany({
      where: {
        authors: {
          hobby: {
            $contains: 'Dance'
          },
        }
      },
    });
    ctx.body = entries;
  }
};

To try the above query, you may want to create a controller, as demonstrated in the video below.

  • Execute the following command in the terminal to create a controller named dancebooks:
npx strapi generate
  • Select the following options:
? Strapi Generators controller - Generate a controller for an API
? Controller name dancebooks
? Where do you want to add this controller? Add controller to new API
++ /api/dancebooks/controllers/dancebooks.js
  • Create a directory for routes:
mkdir src/api/dancebooks/routes
  • In the routes directory, create a file called dancebooks.js and add the following route:
module.exports = {
  routes: [
    {
      method: 'GET',
      path: '/dance-books',
      handler: 'dancebooks.index'
    }
  ]
}
  • Replace the content of src/api/dancebooks/controllers/dancebooks.js with the following lines of code:
'use strict';

module.exports = {
  async index(ctx) {
    try {
      const entries = await strapi.db.query('api::book.book').findMany({
        where: {
          authors: {
            hobby: {
              $contains: 'Dance',
            },
          },
        },
      });

      ctx.body = entries;  // Send the query results
    } catch (err) {
      ctx.throw(500, 'Failed to fetch dance books');
    }
  },
};

Once the server restarts and a GET request is made to /api/dance-books, the expected response will be received. Permission must be granted to the public to access the route.

[
    {
        "id": 5,
        "documentId": "mfnvnveobtf4fzporv9k2z3x",
        "title": "How to Dance",
        "createdAt": "2025-01-16T05:49:27.517Z",
        "updatedAt": "2025-01-16T05:49:27.517Z",
        "publishedAt": null,
        "locale": null
    },
    {
        "id": 6,
        "documentId": "mfnvnveobtf4fzporv9k2z3x",
        "title": "How to Dance",
        "createdAt": "2025-01-16T05:49:27.517Z",
        "updatedAt": "2025-01-16T05:49:27.517Z",
        "publishedAt": "2025-01-16T05:49:27.522Z",
        "locale": null
    },
    {
        "id": 7,
        "documentId": "kte9yxf6oami3680br3gapfl",
        "title": "How to be Flexible",
        "createdAt": "2025-01-16T05:49:53.037Z",
        "updatedAt": "2025-01-16T06:06:43.838Z",
        "publishedAt": null,
        "locale": null
    },
    {
        "id": 10,
        "documentId": "kte9yxf6oami3680br3gapfl",
        "title": "How to be Flexible",
        "createdAt": "2025-01-16T05:49:53.037Z",
        "updatedAt": "2025-01-16T06:06:43.838Z",
        "publishedAt": "2025-01-16T06:06:43.851Z",
        "locale": null
    }
]

Deep Filtering Using the Entity Service API

The same result can be achieved using the Entity Service API. Replace the code in src/api/dancebooks/controllers/dancebooks.js with the following:

'use strict';

 module.exports = {
   async index(ctx, next) {
    const entries = await strapi.entityService.findMany('api::book.book', {
      filters: {
        authors: {
          hobby: {
            $contains: 'Dance'
          },
        }
      },
    });
    ctx.body = entries;
  }
};

Restart the server and send a GET request to /api/dance-books. The expected response is as follows:

[
    {
        "id": 5,
        "documentId": "mfnvnveobtf4fzporv9k2z3x",
        "title": "How to Dance",
        "createdAt": "2025-01-16T05:49:27.517Z",
        "updatedAt": "2025-01-16T05:49:27.517Z",
        "publishedAt": null,
        "locale": null
    },
    {
        "id": 7,
        "documentId": "kte9yxf6oami3680br3gapfl",
        "title": "How to be Flexible",
        "createdAt": "2025-01-16T05:49:53.037Z",
        "updatedAt": "2025-01-16T06:06:43.838Z",
        "publishedAt": null,
        "locale": null
    }
]

The Entity Service API is built upon the Query Engine API. The Entity Service layer handles Strapi's complex data structures, such as components and dynamic zones, using the Query Engine API to execute database queries.

Deep Filtering Using the GraphQL API

  • Install the GraphQL plugin in your Strapi project:
npm install @strapi/plugin-graphql

The following GraphQL query will return all books where the author's hobby contains the word “dance”:

query {
  books (filters: { authors:{hobby : { contains: "dance" }}} ){
    authors {
      hobby
    }
    title
  }
}

The GraphQL Playground should resemble the image below:

A GraphQL playground demonstrating the deep-filtering capabilities of Strapi.png

Deep Filtering Using the Document Service API

The Document Service API is built on top of the Query Engine API and is used to perform CRUD operations on documents: create, retrieve, update, and delete.

Replace the contents of src/api/dancebooks/controllers/dancebooks.js with the following:

'use strict';

module.exports = {
  async index(ctx) {
    try {
      const entries = await strapi.documents('api::book.book').findMany({
        filters: {
          authors: {
            hobby: {
              $contains: 'Dance',
            },
          },
        },
      });

      ctx.body = entries;
    } catch (error) {
      ctx.throw(500, 'Failed to fetch dance books');
    }
  },
};

Restart the server and send a GET request to /api/dance-books. The expected response is as follows:

[
    {
        "id": 5,
        "documentId": "mfnvnveobtf4fzporv9k2z3x",
        "title": "How to Dance",
        "createdAt": "2025-01-16T05:49:27.517Z",
        "updatedAt": "2025-01-16T05:49:27.517Z",
        "publishedAt": null,
        "locale": null
    },
    {
        "id": 7,
        "documentId": "kte9yxf6oami3680br3gapfl",
        "title": "How to be Flexible",
        "createdAt": "2025-01-16T05:49:53.037Z",
        "updatedAt": "2025-01-16T06:06:43.838Z",
        "publishedAt": null,
        "locale": null
    }
]

The Document Service API is meant to replace the Entity Service API.

GitHub Source Code

The source code for this article can be found in this GitHub Repo.

Conclusion

This article demonstrated how to filter deeply nested data using relationship fields in Strapi. You can learn more about these exciting features of Strapi in this excellent blog: What is New for Developers in Strapi 5: Top 10 Changes. Let me know if you have any suggestions and what you plan to build with this knowledge.

Vivek M AgarwalJavascript Engineer & Trainer

When I’m not working, I love to write, learn something new & read non-fiction.

Free Preview, Growth Plan and SSO Add-on
Releases & Roadmap·9 min read

Introducing the Free Preview Feature, Growth Plan, and SSO Add-On

From live Free Previews to enhanced collaboration with the Growth Plan and streamlined access via the SSO Add-On—find out how these features can take your projects to the next level.

·January 15, 2025
Strapi Cloud Regions, CLI Enhancements, Plugins, and Releases x Review
Releases & Roadmap·9 min read

Introducing new Cloud Regions, CLI Enhancements, Plugins, and Releases x Review Workflows

Explore Strapi’s newest features: faster Cloud Regions, enhanced CLI, powerful plugins, and automated content approvals—designed to streamline your content management.

·November 20, 2024
Releases & Roadmap·8 min read

Build, Test, and Deploy Confidently with Multi-Environments for Strapi Cloud

Safely build, test, and deploy with multi-environment support in Strapi Cloud. Isolate your dev stages, track API usage, and manage environments effortlessly with secure backups and custom variables.

·September 27, 2024