Introduction
Hello everyone! This discussion is based on Richard's Strapi Conf talk on demystifying Strapi's populated filtering which was originally based in Strapi 4. Now that we are on Strapi 5, I've updated this blog post to cover what has changed and highlight the key differences you need to know.
This blog post serves as a quick refresher on effectively consuming data in Strapi using field selection, population, and filtering.
What's New in Strapi 5?
Strapi 5 introduces several significant changes to the REST API, most notably a flattened response format and the introduction of documentId as the primary identifier. We'll highlight these changes throughout the post with callout boxes like this one.
Why is This Discussion Necessary?
Over the years, Strapi has evolved significantly, and the most noticeable change has been in how we interact with our content. Content consumption is a crucial part of the data interaction experience. This post aims to help you become comfortable with the tools you need to interact with your data efficiently.
You can learn about these topics in our documentation. Check out the following sections, populate and filtering, as well as, this video where we discuss the topic in more details.
Setting Up the Example Project
To follow along with the examples in this post, we'll use a starter project that includes both a Strapi 5 backend and a Next.js frontend. You can find the project here.
Clone the Repository
Start by cloning the project:
git clone https://github.com/PaulBratslavsky/strapi-5-next-js-starter-project.git
Install Dependencies
Navigate into the project directory and run the setup script to install all dependencies:
cd strapi-5-next-js-starter-project
npm run setup
Seed the Database
Next, populate the database with example data by running the seed command:
npm run seed
You should see output similar to the following, confirming that the data has been imported:
┌─────────────────────────────────────────┬───────┬───────────────┐
│ Type │ Count │ Size │
├─────────────────────────────────────────┼───────┼───────────────┤
│ entities │ 50 │ 51.9 KB │
├─────────────────────────────────────────┼───────┼───────────────┤
│ -- api::category.category │ 4 │ ( 1 KB) │
├─────────────────────────────────────────┼───────┼───────────────┤
│ -- api::global.global │ 2 │ ( 2.1 KB) │
├─────────────────────────────────────────┼───────┼───────────────┤
│ -- api::landing-page.landing-page │ 2 │ ( 8 KB) │
├─────────────────────────────────────────┼───────┼───────────────┤
│ -- api::page.page │ 2 │ ( 3 KB) │
├─────────────────────────────────────────┼───────┼───────────────┤
│ -- api::post.post │ 10 │ ( 20.6 KB) │
├─────────────────────────────────────────┼───────┼───────────────┤
│ -- plugin::upload.file │ 6 │ ( 9.6 KB) │
├─────────────────────────────────────────┼───────┼───────────────┤
│ assets │ 30 │ 4.9 MB │
├─────────────────────────────────────────┼───────┼───────────────┤
│ Total │ 230 │ 5.1 MB │
└─────────────────────────────────────────┴───────┴───────────────┘
Import process has been completed successfully!
Start the Application
Now you're ready to start both the frontend and backend:
npm run dev
- Strapi Admin Panel: http://localhost:1337/admin — You'll be prompted to create your first admin user.
- Next.js Frontend: http://localhost:3000
Once you've created your admin account, you'll see the Strapi admin panel:

And the frontend will be running at:
Getting Started with Population and Field Selection
Now let's explore how population and field selection work. For this, we will focus on populating our post with the following documentId cqaabo4pxlrhrjjhxbima0hq.
If you make a GET request to the post endpoint at http://localhost:1337/api/posts/cqaabo4pxlrhrjjhxbima0hq, you'll see this response:
{
"data": {
"id": 23,
"documentId": "cqaabo4pxlrhrjjhxbima0hq",
"title": "How to Choose the Right CMS for Your Business",
"description": "A comprehensive guide to help businesses select the best CMS to meet their needs, whether they require a simple website builder or a custom headless CMS solution.",
"slug": "how-to-choose-the-right-cms-for-your-business",
"content": "Choosing the right CMS (Content Management System) for your business is crucial...",
"createdAt": "2024-07-20T15:42:31.381Z",
"updatedAt": "2026-01-27T05:09:55.493Z",
"publishedAt": "2026-01-27T05:09:55.502Z"
},
"meta": {}
}
Notice that the response only includes the top-level fields: title, description, slug, and content.
What are top-level fields? By default, Strapi's REST API only returns scalar field types — the simple values stored directly on the content type. According to the Strapi documentation, these include:
- String types: string, text, richtext, enumeration, email, password, uid
- Date types: date, time, datetime, timestamp
- Number types: integer, biginteger, float, decimal
- Generic types: boolean, array, JSON
Relations, media fields, components, and dynamic zones are not included by default. These require explicit population because they involve additional database queries to fetch data from related tables.
This explains why our API response only returned the basic fields. If you look at the Post collection in the Strapi Admin UI, you'll see additional items — relations, media, components, and dynamic zones — that weren't included in the response.
For instance, above we can see that we have an image, but this was not returned in our API request.
This is by design. Strapi does not automatically populate relations, components, or dynamic zones. You must explicitly request them.
This approach helps optimize performance by reducing unnecessary database queries and keeping API responses lean.
Permissions Matter
To access any content type via the API, you need to enable the appropriate permissions. In our case, the find and findOne permissions are already enabled for the post collection:
Without these permissions, you'd receive a 403 Forbidden error:
{
"data": null,
"error": {
"status": 403,
"name": "ForbiddenError",
"message": "Forbidden",
"details": {}
}
}
Key Takeaways
When working with populate and filtering in Strapi, keep these points in mind:
- Relations are not populated by default — you must explicitly request them
- Permissions must be enabled for any content type you want to populate
- Deep population impacts performance — the more levels deep you populate, the longer queries take
This design gives you granular control over your API responses and helps prevent over-fetching unnecessary data.
Exploring Population and Field Selection with Examples
Let's go through some examples using our local project that we just setup.
To help with this, we will be using Strapi's Query Builder tool, which you can find here.
Here is a helpful Chrome extension to make your JSON more beautiful when viewing in the browser. JSON Viewer
In my Strapi application, I have created several collection types and relations, which I will explain as we proceed. We have top-level relations, top-level fields, dynamic zones, and components within those dynamic zones.
Let's take a look at some examples of population and field selection.
Populate Everything First Level Deep with the Wildcard Operator
Before looking at the wildcard * operator, let's look at what we get as a response when querying our posts without passing and populating or filtering options.
You can run this GET query in Postman, but we will just look at it in the browser for brevity.
Post Query: http://localhost:1337/api/posts/cqaabo4pxlrhrjjhxbima0hq
As we saw earlier, this returns only the top-level fields — no relations, components, or dynamic zones:
{
"data": {
"id": 23,
"documentId": "cqaabo4pxlrhrjjhxbima0hq",
"title": "How to Choose the Right CMS for Your Business",
"description": "A comprehensive guide to help businesses select the best CMS...",
"slug": "how-to-choose-the-right-cms-for-your-business",
"content": "Choosing the right CMS (Content Management System)...",
"createdAt": "2024-07-20T15:42:31.381Z",
"updatedAt": "2026-01-27T05:09:55.493Z",
"publishedAt": "2026-01-27T05:09:55.502Z"
},
"meta": {}
}
Strapi 5 Change: Flattened Response Format
In Strapi 4, the response wrapped all fields inside a data.attributes object:
{
"data": {
"id": 1,
"attributes": {
"title": "My Article",
"slug": "my-article"
}
}
}
In Strapi 5, the response is flattened — fields like title and slug are directly on the data object. Strapi 5 also introduces documentId as the primary identifier for API calls.
This means you no longer need to access data.attributes.title — you can simply use data.title. This simplifies frontend code significantly.
You can learn more about the changes between Strapi 4 to Strapi 5 here
If you want to bring back as much information as possible, use the wildcard * operator.
{
populate: "*",
}
What is LHS Notation?
Throughout this post, you'll see queries written in two formats: object notation (the JavaScript object above) and LHS Bracket notation (the URL query string below).
LHS (Left-Hand Side) Bracket notation is the format used in URL query strings to represent nested objects. For example, populate[author][fields][0]=name represents { populate: { author: { fields: ["name"] } } }.
Strapi uses the qs library to parse these query strings. You can use Strapi's Interactive Query Builder to convert between object notation and LHS notation — it's a helpful tool for constructing complex queries.
Alternatively, you can use the official Strapi Client on your frontend, which handles query building and a way to interact with your Strapi API.
LHS Notation: http://localhost:1337/api/posts/cqaabo4pxlrhrjjhxbima0hq?populate=*
Remember that some details may not be visible, such as image in our SEO component due to limited population depth.
{
"data": {
"id": 23,
"documentId": "cqaabo4pxlrhrjjhxbima0hq",
"title": "How to Choose the Right CMS for Your Business",
"description": "A comprehensive guide to help businesses select the best CMS to meet their needs, whether they require a simple website builder or a custom headless CMS solution.",
"slug": "how-to-choose-the-right-cms-for-your-business",
"content": "Choosing the right CMS (Content Management System) for your business is crucial for your online success. With so many options available, it can be overwhelming to make the right choice. This guide will help you navigate the various types of CMS and select the one that best fits your needs.\n\n### Types of CMS\n1. **Traditional CMS**: Ideal for small to medium-sized websites. Examples include WordPress and Joomla.\n2. **Website Builders**: Perfect for those who need a quick setup with minimal technical knowledge. Examples include Wix and Squarespace.\n3. **Headless CMS**: Suitable for businesses requiring a custom solution with more flexibility and scalability. Examples include Strapi and Contentful.\n\n### Factors to Consider\n- **Ease of Use**: Consider the learning curve associated with each CMS.\n- **Flexibility**: Determine if the CMS can handle your business's growth and evolving needs.\n- **Cost**: Evaluate your budget and the total cost of ownership, including maintenance and scalability.\n\n### Conclusion\nSelecting the right CMS requires careful consideration of your business's specific needs and goals. By understanding the different types of CMS and their features, you can make an informed decision that will support your business's online presence.\n",
"createdAt": "2024-07-20T15:42:31.381Z",
"updatedAt": "2026-01-27T05:09:55.493Z",
"publishedAt": "2026-01-27T05:09:55.502Z",
"image": {
"id": 10,
"documentId": "nld6jhxi2jlu95hkb8wv28zv",
"name": "pexels-cottonbro-4855327.jpg",
"alternativeText": null,
"caption": null,
"width": 3500,
"height": 2333,
"formats": {
"thumbnail": {
"name": "thumbnail_pexels-cottonbro-4855327.jpg",
"hash": "thumbnail_pexels_cottonbro_4855327_5fb6b05354",
"ext": ".jpg",
"mime": "image/jpeg",
"path": null,
"width": 234,
"height": 156,
"size": 8.81,
"sizeInBytes": 8808,
"url": "/uploads/thumbnail_pexels_cottonbro_4855327_5fb6b05354.jpg"
},
"small": {
"name": "small_pexels-cottonbro-4855327.jpg",
"hash": "small_pexels_cottonbro_4855327_5fb6b05354",
"ext": ".jpg",
"mime": "image/jpeg",
"path": null,
"width": 500,
"height": 333,
"size": 28.39,
"sizeInBytes": 28392,
"url": "/uploads/small_pexels_cottonbro_4855327_5fb6b05354.jpg"
},
"medium": {
"name": "medium_pexels-cottonbro-4855327.jpg",
"hash": "medium_pexels_cottonbro_4855327_5fb6b05354",
"ext": ".jpg",
"mime": "image/jpeg",
"path": null,
"width": 750,
"height": 500,
"size": 54.32,
"sizeInBytes": 54324,
"url": "/uploads/medium_pexels_cottonbro_4855327_5fb6b05354.jpg"
},
"large": {
"name": "large_pexels-cottonbro-4855327.jpg",
"hash": "large_pexels_cottonbro_4855327_5fb6b05354",
"ext": ".jpg",
"mime": "image/jpeg",
"path": null,
"width": 1000,
"height": 666,
"size": 84.83,
"sizeInBytes": 84826,
"url": "/uploads/large_pexels_cottonbro_4855327_5fb6b05354.jpg"
}
},
"hash": "pexels_cottonbro_4855327_5fb6b05354",
"ext": ".jpg",
"mime": "image/jpeg",
"size": 593.5,
"url": "/uploads/pexels_cottonbro_4855327_5fb6b05354.jpg",
"previewUrl": null,
"provider": "local",
"provider_metadata": null,
"createdAt": "2024-08-20T15:29:58.080Z",
"updatedAt": "2025-05-01T04:45:33.508Z",
"publishedAt": "2024-08-20T15:29:58.086Z"
},
"category": {
"id": 8,
"documentId": "kbumsa4dtqk7v2jqlfehjrfx",
"text": "strapi",
"description": "All posts about Strapi.",
"createdAt": "2024-07-20T19:35:02.052Z",
"updatedAt": "2024-07-20T19:35:02.052Z",
"publishedAt": "2024-07-20T19:35:02.055Z"
},
"blocks": [
{
"__component": "blocks.video",
"id": 13,
"title": "Strapi 5 Crashcourse",
"description": "Everything you need to get started with Strapi 5",
"videoUrl": "https://www.youtube.com/watch?v=t1iUuap7vhw",
"video": {
"videoId": "t1iUuap7vhw",
"start": "",
"end": ""
}
},
{
"__component": "blocks.text",
"id": 13,
"content": "**The standard Lorem Ipsum passage, used since the 1500s**\"\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\"\n\n**Section 1.10.32 of \"de Finibus Bonorum et Malorum\", written by Cicero in 45 BC\"**\n\nSed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?\"\n\n"
}
],
"seo": {
"id": 6,
"metaTitle": "How to Choose the Right CMS for Your Business",
"metaDescription": "Choosing the right CMS (Content Management System) for your business.",
"keywords": null,
"metaRobots": null,
"metaViewport": null,
"canonicalURL": null,
"structuredData": null
}
},
"meta": {}
}
When using the wildcard * operator, you will only populate relations, dynamic zones, and components to a depth of one.
You can learn more in our documentation on the wildcard operator here.
Why You Shouldn't Always Use populate=*
While the wildcard operator is convenient for quick testing, it's not recommended for production use. Here's why:
- Performance impact — It fetches all relations, media, components, and dynamic zones, even if you don't need them. This increases database query time and response payload size.
- Only one level deep — It doesn't populate nested relations, so you may still need explicit population for deeper data.
- Security concerns — You might accidentally expose data that shouldn't be public.
Instead, explicitly populate only the fields you need. This gives you better performance, smaller payloads, and more predictable API responses.
populate: true vs populate: "*" on Dynamic Zones
When populating specific fields, you might use populate: true or populate: "*" — for most relations and components these behave similarly. However, there is an important difference when working with dynamic zones:
-
populate: { blocks: true }— Populates the dynamic zone one level deep (returns component fields but not their nested relations). -
populate: { blocks: { populate: "*" } }— Populates the dynamic zone and its nested relations one level deeper. This works. -
populate: { blocks: { populate: true } }— This causes a 500 error on dynamic zones. Strapi cannot resolve which components to populate generically withtrue.
For dynamic zones, always use "*" or the explicit on syntax (shown later) when you need to go deeper than one level. The populate: true shorthand only works on regular relations and components.
Populate Specific Relations
To populate specific relations, we must specify which fields we want to populate in Strapi. Let's take a look at the following example.
Instead of using the wildcard, we will build out the query manually. Here is what our current query looks like without any options being passed in Strapi's interactive query builder that you can find here.
Let's add this populate query. We can use this array populate notation to specify which fields we want to populate. The following will get the same data as when we used the wildcard and populated one level deep for all the specified items.
{
populate: [
"image",
"category",
"seo",
"blocks"
],
}
or using the object notation
{
populate: {
image: true,
category: true,
seo: true,
blocks: true,
},
}
or
{
populate: {
image: {
populate: true
},
category: {
populate: true
},
seo: {
populate: true
},
blocks: true
},
}
In most complex queries, object notation is preferred because of this flexibility.
LHS Notation: http://localhost:1337/api/posts/cqaabo4pxlrhrjjhxbima0hq?populate[image]=true&populate[category]=true&populate[seo]=true&populate[blocks]=true
Before looking at how we can populate nested relations, let's look at how we can populate specific fields.
Populate Nested Relations
Let's look at how we can populate our nested relationships. We will use the above query as a starting point.
We will look at how to populate the metaImage media field nested inside our seo component.
When we used populate=* earlier, the seo component was populated one level deep — but the metaImage inside it was not returned because it requires an additional level of population.
{
populate: [
"image",
"category",
"blocks",
"seo.metaImage"
],
}
or using the object notation
{
populate: {
image: true,
category: true,
blocks: true,
seo: {
populate: ["metaImage"]
}
},
}
LHS Notation: http://localhost:1337/api/posts/cqaabo4pxlrhrjjhxbima0hq?populate[image]=true&populate[category]=true&populate[blocks]=true&populate[seo][populate][metaImage]=true
In the response below, we can see the seo component now includes the nested metaImage relation:
{
"data": {
"id": 23,
"documentId": "cqaabo4pxlrhrjjhxbima0hq",
"title": "How to Choose the Right CMS for Your Business",
"seo": {
"id": 6,
"metaTitle": "How to Choose the Right CMS for Your Business",
"metaDescription": "Choosing the right CMS (Content Management System) for your business.",
"keywords": null,
"metaImage": {
"id": 10,
"documentId": "nld6jhxi2jlu95hkb8wv28zv",
"name": "pexels-cottonbro-4855327.jpg",
"alternativeText": null,
"url": "/uploads/pexels_cottonbro_4855327_5fb6b05354.jpg"
}
}
}
}
Notice how the metaImage is now fully populated with all its fields. Without the nested populate, the seo component would only show its scalar fields and the metaImage would be missing.
Strapi 5 Change: Flattened Relations
In Strapi 4, related data was nested inside
data.attributes:{ "seo": { "id": 6, "metaTitle": "My Title", "metaImage": { "data": { "id": 10, "attributes": { "url": "/uploads/image.jpg" } } } } }In Strapi 5, relations are flattened and use
documentId:{ "seo": { "id": 6, "metaTitle": "My Title", "metaImage": { "documentId": "nld6jhxi2jlu95hkb8wv28zv", "url": "/uploads/image.jpg" } } }
There are a lot of fields. What if we wanted to populate selected fields only. That is exactly what I will show you in the next section.
Populate and Field Select
We will now see how you can tell Strapi only to return specific fields. We will also see where the object notation is useful since array notation does not allow you to do this.
Array notation example:
{
populate: [
"image",
"category",
"blocks",
"seo.metaImage"
],
}
Object notation example:
{
populate: {
image: true,
category: true,
blocks: true,
seo: {
populate: ["metaImage"]
}
},
}
Let's refactor our object notation example to see how to populate only specific fields. We will focus on the image media field.
When we populated image earlier, it returned all of its fields — name, alternativeText, caption, width, height, formats, hash, ext, mime, size, url, and more. That's a lot of data we may not need.
Let's now only return the name, alternativeText, and url.
To accomplish this, let's make the following changes to our query.
{
populate: {
image: {
fields: [
"name",
"alternativeText",
"url"
]
},
category: true,
blocks: true,
seo: {
populate: {
metaImage: {
fields: ["url", "alternativeText"]
}
}
}
},
}
LHS Notation: http://localhost:1337/api/posts/cqaabo4pxlrhrjjhxbima0hq?populate[image][fields][0]=name&populate[image][fields][1]=alternativeText&populate[image][fields][2]=url&populate[category]=true&populate[seo][populate][metaImage][fields][0]=url&populate[seo][populate][metaImage][fields][1]=alternativeText&populate[blocks]=true
Notice in the response below we returned all of our data from before, but only the specified fields for our image and seo.metaImage:
{
"data": {
"id": 23,
"documentId": "cqaabo4pxlrhrjjhxbima0hq",
"title": "How to Choose the Right CMS for Your Business",
"image": {
"id": 10,
"documentId": "nld6jhxi2jlu95hkb8wv28zv",
"name": "pexels-cottonbro-4855327.jpg",
"alternativeText": null,
"url": "/uploads/pexels_cottonbro_4855327_5fb6b05354.jpg"
},
"seo": {
"id": 6,
"metaTitle": "How to Choose the Right CMS for Your Business",
"metaDescription": "Choosing the right CMS (Content Management System) for your business.",
"metaImage": {
"id": 10,
"documentId": "nld6jhxi2jlu95hkb8wv28zv",
"url": "/uploads/pexels_cottonbro_4855327_5fb6b05354.jpg",
"alternativeText": null
}
}
}
}
Instead of getting all the image format data (thumbnail, small, medium, large), we only get exactly what we need — a much leaner response.
In the next section, we will look at complex population and field selection.
Complex Population and Field Selection
In this example, instead of getting our posts, we will look to populate our landing page. The landing page is a single type (accessed at /api/landing-page) that uses a blocks dynamic zone with several different components:
-
layout.hero— heading, text, topLink (link component), buttonLink (repeatable link component), image (media) -
layout.section-heading— subHeading, heading, text (all scalar fields) -
layout.card-grid— cardItems (repeatable card component with icon, heading, text) -
layout.content-with-image— heading, subHeading, text, image (media), reverse (boolean) -
layout.price-grid— priceCard (repeatable price-card component with heading, description, price, feature[], link)
We will build this query in steps.
Step 1: Basic Wildcard Populate
First, let's see what the landing page looks like with a simple wildcard populate:
{
populate: "*",
}
LHS Notation: http://localhost:1337/api/landing-page?populate=*
This returns all blocks populated one level deep. We can see the different components in the blocks dynamic zone, but nested relations like the image inside the hero or cardItems inside card-grid are not included.
Step 2: Populate All Nested Data with on
To get all the data from every component in the dynamic zone, we need to use the on option to target each component and populate its nested relations.
You can see our docs for additional information here.
{
populate: {
blocks: {
on: {
"layout.hero": {
populate: {
topLink: true,
buttonLink: true,
image: {
fields: ["url", "alternativeText"]
}
}
},
"layout.card-grid": {
populate: {
cardItems: true
}
},
"layout.section-heading": {
populate: "*"
},
"layout.content-with-image": {
populate: {
image: {
fields: ["url", "alternativeText"]
}
}
},
"layout.price-grid": {
populate: {
priceCard: {
populate: {
feature: true,
link: true
}
}
}
}
}
}
}
}
LHS Notation: http://localhost:1337/api/landing-page?populate[blocks][on][layout.hero][populate][topLink]=true&populate[blocks][on][layout.hero][populate][buttonLink]=true&populate[blocks][on][layout.hero][populate][image][fields][0]=url&populate[blocks][on][layout.hero][populate][image][fields][1]=alternativeText&populate[blocks][on][layout.card-grid][populate][cardItems]=true&populate[blocks][on][layout.section-heading][populate]=*&populate[blocks][on][layout.content-with-image][populate][image][fields][0]=url&populate[blocks][on][layout.content-with-image][populate][image][fields][1]=alternativeText&populate[blocks][on][layout.price-grid][populate][priceCard][populate][feature]=true&populate[blocks][on][layout.price-grid][populate][priceCard][populate][link]=true
This returns all the landing page data with every component fully populated. Here's a condensed view of the response showing each component type:
{
"data": {
"id": 4,
"documentId": "l6etcff49lp5c1v21lpv3vwd",
"title": "Landing Page",
"description": "This is the main landing page.",
"blocks": [
{
"__component": "layout.hero",
"id": 6,
"heading": "Next.js 15 and Strapi 5 Starter Project",
"text": "Give Strapi 5 and Next.js a spin with this starter project...",
"topLink": {
"id": 49,
"href": "https://github.com/PaulBratslavsky/strapi-5-next-js-starter-project",
"text": "GitHub Project Repo",
"isExternal": true,
"isPrimary": false
},
"buttonLink": [
{ "id": 50, "href": "https://strapi.io/five", "text": "Strapi 5", "isExternal": true, "isPrimary": false },
{ "id": 51, "href": "https://docs-next.strapi.io/dev-docs/whats-new", "text": "Strapi 5 Docs", "isExternal": true, "isPrimary": true }
],
"image": { "id": 9, "documentId": "sg6vq9j5il16fl8y38dob4c8", "url": "/uploads/strapi_dashboard_d4cf37208d.png", "alternativeText": null }
},
{
"__component": "layout.section-heading",
"id": 7,
"subHeading": "Strapi 5 Features",
"heading": "Build fast and stay flexible",
"text": "Strapi 5 brings many new features and improvements.\n"
},
{
"__component": "layout.card-grid",
"id": 6,
"cardItems": [
{ "id": 16, "icon": "Frame", "heading": "DRAFT & PUBLISH", "text": "Reduce the risk of publishing errors..." },
{ "id": 17, "icon": "Download", "heading": "CONTENT HISTORY", "text": "Overcome the risks of data loss..." },
{ "id": 18, "icon": "Globe", "heading": "100% TYPESCRIPT", "text": "Easier bug detection..." }
]
},
{
"__component": "layout.content-with-image",
"id": 7,
"heading": "Designed to build fast",
"subHeading": "BUILD FAST",
"text": "Strapi is designed to be highly customizable out of the box...",
"reverse": true,
"image": { "id": 9, "documentId": "sg6vq9j5il16fl8y38dob4c8", "url": "/uploads/strapi_dashboard_d4cf37208d.png", "alternativeText": null }
},
{
"__component": "layout.price-grid",
"id": 4,
"priceCard": [
{
"id": 10,
"selected": false,
"heading": "Developer",
"description": "Perfect for getting started",
"price": "29",
"feature": [
{ "id": 67, "description": "1 seat" },
{ "id": 68, "description": "1000 CMS Entries" }
],
"link": { "id": 52, "href": "https://strapi.io/cloud", "text": "Get started", "isExternal": true, "isPrimary": true }
}
]
}
]
},
"meta": {}
}
Notice the different levels of nesting: the price-grid component has priceCard items, each containing feature components and a link component — that's three levels deep from the landing page root. Without the explicit on population, none of this nested data would be returned.
Step 3: Selective Population with on
But what if you only need specific components from the dynamic zone? The on option also lets you exclude components you don't need. For example, if you only want the hero and card-grid:
{
populate: {
blocks: {
on: {
"layout.hero": {
fields: ["heading", "text"],
populate: {
image: {
fields: ["url", "alternativeText"]
},
buttonLink: true
}
},
"layout.card-grid": {
populate: {
cardItems: {
fields: ["heading", "text"]
}
}
}
}
}
}
}
LHS Notation: http://localhost:1337/api/landing-page?populate[blocks][on][layout.hero][fields][0]=heading&populate[blocks][on][layout.hero][fields][1]=text&populate[blocks][on][layout.hero][populate][image][fields][0]=url&populate[blocks][on][layout.hero][populate][image][fields][1]=alternativeText&populate[blocks][on][layout.hero][populate][buttonLink]=true&populate[blocks][on][layout.card-grid][populate][cardItems][fields][0]=heading&populate[blocks][on][layout.card-grid][populate][cardItems][fields][1]=text
Now the response only includes the hero and card-grid components — all other components (section-heading, content-with-image, price-grid) are excluded. Combined with field selection, this gives you a much leaner response:
{
"data": {
"id": 4,
"documentId": "l6etcff49lp5c1v21lpv3vwd",
"title": "Landing Page",
"description": "This is the main landing page.",
"blocks": [
{
"__component": "layout.hero",
"id": 6,
"heading": "Next.js 15 and Strapi 5 Starter Project",
"text": "Give Strapi 5 and Next.js a spin with this starter project...",
"image": { "id": 9, "documentId": "sg6vq9j5il16fl8y38dob4c8", "url": "/uploads/strapi_dashboard_d4cf37208d.png", "alternativeText": null },
"buttonLink": [
{ "id": 50, "href": "https://strapi.io/five", "text": "Strapi 5", "isExternal": true, "isPrimary": false },
{ "id": 51, "href": "https://docs-next.strapi.io/dev-docs/whats-new", "text": "Strapi 5 Docs", "isExternal": true, "isPrimary": true }
]
},
{
"__component": "layout.card-grid",
"id": 6,
"cardItems": [
{ "id": 16, "heading": "DRAFT & PUBLISH", "text": "Reduce the risk of publishing errors..." },
{ "id": 17, "heading": "CONTENT HISTORY", "text": "Overcome the risks of data loss..." },
{ "id": 18, "heading": "100% TYPESCRIPT", "text": "Easier bug detection..." }
]
}
]
},
"meta": {}
}
The on option gives you precise control over which dynamic zone components are returned and what data they include. This is incredibly useful for building frontend pages where you only need specific sections.
Now that we know how to populate individual components from our dynamic zones, we will look at how to apply filtering to our query in the next section.
Filtering Data
Filtering allows you to retrieve specific information from your application. However, note that deep filtering is not available in dynamic zones, and complex filters may cause performance issues. You can learn more about filtering here.
Strapi 5 Change: New Filter Operators
Strapi 5 introduces several new case-insensitive operators that weren't available in Strapi 4:
New Operator Description $eqiEqual (case-insensitive) $neiNot equal (case-insensitive) $containsiContains (case-insensitive) $notContainsiDoes not contain (case-insensitive) $startsWithiStarts with (case-insensitive) $endsWithiEnds with (case-insensitive) $betweenBetween two values (range) These new operators make filtering much more flexible, especially for user-facing search functionality.
Here are some filtering examples:
Top-level Filtering Example
Let's filter our posts by slug to find a specific post.
{
filters: {
slug: "how-to-choose-the-right-cms-for-your-business"
}
}
LHS Notation: http://localhost:1337/api/posts?filters[slug]=how-to-choose-the-right-cms-for-your-business
By default, the above notation uses the $eq operator; we can also write this with the following.
{
filters: {
slug: {
'$eq': "how-to-choose-the-right-cms-for-your-business"
}
}
}
LHS Notation: http://localhost:1337/api/posts?filters[slug][$eq]=how-to-choose-the-right-cms-for-your-business
Here are some examples of common operators.
| Operator | Description |
|---|---|
$eq |
Equal |
$eqi |
Equal (case-insensitive) - New in Strapi 5 |
$ne |
Not equal |
$nei |
Not equal (case-insensitive) - New in Strapi 5 |
$lt |
Less than |
$lte |
Less than or equal to |
$gt |
Greater than |
$gte |
Greater than or equal to |
$in |
Included in an array |
$notIn |
Not included in an array |
$contains |
Contains |
$notContains |
Does not contain |
$containsi |
Contains (case-insensitive) - New in Strapi 5 |
$notContainsi |
Does not contain (case-insensitive) - New in Strapi 5 |
$startsWith |
Starts with |
$startsWithi |
Starts with (case-insensitive) - New in Strapi 5 |
$endsWith |
Ends with |
$endsWithi |
Ends with (case-insensitive) - New in Strapi 5 |
$null |
Is null |
$notNull |
Is not null |
$between |
Between two values - New in Strapi 5 |
$or |
Joins the filters in an "or" expression |
$and |
Joins the filters in an "and" expression |
$not |
Joins the filters in an "not" expression |
You can see the complete list here
Let's look for posts where the seo component has not been filled in (is null). In our dataset, 4 out of 5 posts don't have SEO data.
We can run the following query.
{
filters: {
seo: {
"$null": true
}
},
fields: ["title", "slug"]
}
LHS Notation: http://localhost:1337/api/posts?filters[seo][$null]=true&fields[0]=title&fields[1]=slug
In the response, we can see only the posts without SEO data are returned:
{
"data": [
{
"id": 17,
"documentId": "tlqsaexq8gshcyzq44z761t4",
"title": "The Benefits of Using a Headless CMS for Your Website",
"slug": "benefits-of-using-a-headless-cms-for-your-website"
},
{
"id": 18,
"documentId": "phsnnk9tf9s9r8lqfn2l1paf",
"title": "Top 10 Headless CMS Platforms for 2024",
"slug": "top-10-headless-cms-platforms-for-2024"
}
],
"meta": {
"pagination": { "page": 1, "pageSize": 25, "pageCount": 1, "total": 4 }
}
}
Filtering by Relation Example
You can filter based on relations, like listing all the posts that belong to a specific category. We will filter our posts based on the text field in the category relation.
{
fields: ["title"],
populate: {
category: {
fields: ["text"]
}
},
filters: {
category: {
text: {
"$eq": "strapi"
}
}
}
}
Strapi 5 Change: No More
idin Field SelectionIn Strapi 5, you no longer need to explicitly request the
idfield. ThedocumentIdis automatically included in responses. When selecting specific fields, focus on the actual content fields you need.
LHS Notation: http://localhost:1337/api/posts?filters[category][text][$eq]=strapi&populate[category][fields][0]=text&fields[0]=title
In the response below, we can see only posts in the "strapi" category:
{
"data": [
{
"id": 22,
"documentId": "jzo7u6rxoai6lgrstylj51fz",
"title": "Testing video custom field",
"category": {
"id": 8,
"documentId": "kbumsa4dtqk7v2jqlfehjrfx",
"text": "strapi"
}
},
{
"id": 23,
"documentId": "cqaabo4pxlrhrjjhxbima0hq",
"title": "How to Choose the Right CMS for Your Business",
"category": {
"id": 8,
"documentId": "kbumsa4dtqk7v2jqlfehjrfx",
"text": "strapi"
}
}
],
"meta": {
"pagination": { "page": 1, "pageSize": 25, "pageCount": 1, "total": 2 }
}
}
Complex Filtering
You can use complex filters, like combining multiple conditions using the $and operator. Let's refactor the above query to see this in action.
In the following query, we would only like to return posts that belong to the strapi category and have CMS in the title (case-insensitive) using our $and operator. Notice how it takes an array as an argument.
{
fields: ["title"],
populate: {
category: {
fields: ["text"]
}
},
filters: {
$and: [
{ category: { text: { "$eq": "strapi" } } },
{ title: { "$containsi": "CMS" } }
]
}
}
LHS Notation: http://localhost:1337/api/posts?fields[0]=title&populate[category][fields][0]=text&filters[$and][0][category][text][$eq]=strapi&filters[$and][1][title][$containsi]=CMS
In the following response, you can see that we only return posts that belong to the strapi category and contain CMS in the title:
Strapi 5 Response Format
Notice the flattened structure — no more
attributeswrapper:
{
"data": [
{
"id": 23,
"documentId": "cqaabo4pxlrhrjjhxbima0hq",
"title": "How to Choose the Right CMS for Your Business",
"category": {
"id": 8,
"documentId": "kbumsa4dtqk7v2jqlfehjrfx",
"text": "strapi"
}
}
],
"meta": {
"pagination": { "page": 1, "pageSize": 25, "pageCount": 1, "total": 1 }
}
}
Notice how we used $containsi (the case-insensitive contains operator, new in Strapi 5) to match "CMS" regardless of casing in the title.
Migrating from Strapi 4 to Strapi 5
If you're upgrading from Strapi 4, here's a summary of the key changes you need to be aware of:
Migration Checklist: Strapi 4 to Strapi 5
Response Format Changes
- Remove
attributesaccessor — Changedata.attributes.titletodata.title- Use
documentIdinstead ofid— The primary identifier is now a string-baseddocumentId- Update relation access patterns — Relations are flattened, no more
relation.data.attributesTemporary Compatibility
If you need time to update your frontend code, you can use the compatibility header:
curl -H 'Strapi-Response-Format: v4' 'https://your-api.com/api/articles'This will return responses in the old Strapi 4 format while you migrate.
New Features to Leverage
- Use the new case-insensitive operators (
$eqi,$containsi, etc.) for better search- Use
$betweenoperator for date and number range queries- The
documentIdprovides better document tracking across locales
Quick Reference: Response Format Comparison
| Aspect | Strapi 4 | Strapi 5 |
|---|---|---|
| Field Access | data.attributes.title |
data.title |
| ID Field |
data.id (number) |
data.documentId (string) |
| Relation Data | relation.data.attributes.name |
relation.name |
| Array Response | data[0].attributes.title |
data[0].title |
Conclusion
To conclude, today, we demystified populate and filtering in Strapi 5.
We strongly recommend that you are the one who writes the populate and filtering logic in your app and not rely on external plugins such as Populate Deep.
Yes, it is an easy solution, but it comes at the cost of performance since it will populate all the data to the depth specified.
From what we covered above, Strapi's populate and filtering gives you granular control of what data you want to return from your API.
This allows you to only get the data that you need. You can also add the populate and filtering inside a route middleware, allowing you to write less code on your front end.
Here is an excellent article written by Kellen Bolger that you should check out here.
Hope you enjoyed this article. If you want to deploy your own Strapi project, check out Strapi Cloud.
As always, thank you for stopping by, and remember to join the Strapi community on Discord to discuss and learn more.
We have daily open office hours Monday - Friday from 12:30 PM CST to 1:30 PM CST.
Thanks for reading!
Additional Resources
- Starter Project (GitHub) — The Strapi 5 + Next.js project used in this post
- Strapi 5 Populate & Select Documentation
- Strapi 5 Filtering Documentation
- Strapi 5 Migration Guide: New Response Format
- Interactive Query Builder
- Transitioning from Strapi 4 to Strapi 5 FAQ








Top comments (0)