Skip to main content

Command Palette

Search for a command to run...

Calling Strapi REST APIs Fluently

Updated
7 min read
Calling Strapi REST APIs Fluently

When working with Strapi, one of the most powerful and flexible headless CMS platforms out there, we often find ourselves interacting with the Strapi REST API to fetch data. While Strapi’s REST API is straightforward, building complex queries—especially for filtering, sorting, pagination, and population of relations—can become cumbersome and error-prone as the needs of your project grow.

The Old Way

Strapi’s docs on using the REST APIs, including its many features are quite extensive and well-explained, even for complex usecases. To sum it up, the way the REST API handles data querying is by mimicking a database query engine, which is very powerful and allows us to write queries like this:

{
  filters: {
    $or: [
      {
        date: {
          $eq: '2020-01-01',
        },
      },
      {
        date: {
          $eq: '2020-01-02',
        },
      },
    ],
    author: {
      name: {
        $eq: 'Kai doe',
      },
    },
  },
}

Then to make the call you make a GET request by using qs to stringify this into a URL-friendly query string, and you get:

filters[$or][0][date][$eq]=2020-01-01&filters[$or][1][date][$eq]=2020-01-02&filters[author][name][$eq]=Kai%20doe

Now it’s getting sticky.

This way of building queries is quite powerful, but can become very unwieldy when we want to express something in a more programmatic fashion; we have to spread a bunch of JSON objects on a bunch of different depths which can become very painful, very fast.

The “new” way - a fluent query builder

As is tradition in the JS ecosystem when you run into a problem - you build a library.

The goal behind developing this library was simple: to streamline the process of building dynamic, complex queries with a fluent, easy-to-understand API. Whether you’re querying content for a blog, managing a large product catalog, or building a custom CMS with intricate relationships, this library should make working with Strapi’s REST API significantly more efficient.

While Strapi Fluent Query Builder is designed with Strapi in mind (it’s in the name), it’s also meant to be agnostic—it should theoretically work with any RESTful API that adheres to similar conventions. (if you know of any, let me know :))

Let’s take the example we looked at in the previous section:

{
      filters: {
        $or: [
          {
            date: {
              $eq: "2020-01-01",
            },
          },
          {
            date: {
              $eq: "2020-01-02",
            },
          },
        ],
        author: {
          name: {
            $eq: "Kai doe",
          },
        },
      },
    };

The equivalent of this code with the query builder would be:

query("blogs")
  .where("author.name.$eq", "Kai doe")
  .or({
    date: {
      $eq: "2020-01-01",
    },
  })
  .or({
    date: {
      $eq: "2020-01-02",
    },
  });

While it’s not a lot less code, it does provide us with a JS/TS API for interacting with the parameters we want to send to our API, this means we can chain calls and perform any other operations as we would with any other builder.

Key Features

1. Fluent API for Easy Query Building

One of the standout features of the Strapi Fluent Query Builder is its fluent API. This allows you to chain methods in a clean and readable way, making query building intuitive and reducing the need for boilerplate code. You can focus on what you're querying, not how to format the query.

Example:

const result = query('books')
    .where('title.$contains', 'example')
    .sort('createdAt', 'desc')
    .page(1)
    .pageSize(10)
    .get();

2. Advanced Filtering Capabilities

The library offers a flexible filtering system that supports multiple filter types, such as equality ($eq), inclusion ($in), and string matching ($contains). You can easily apply logical operators ($and, $or, $not) to combine multiple conditions and create complex filtering logic.

Example:

const result = query('templates')
    .or({ title: { $eq: 'test' } })
    .or({ tags: { $in: [1, 2, 3] } })
    .get();

This will match either the templates that have exactly the title ‘test’ or that contain the tags 1, 2 or 3.

3. Sorting and Pagination

With the library’s sorting and pagination features, you can precisely control how results are returned. Whether you're displaying a list of blog posts or product pages, you can easily sort by multiple fields and control pagination parameters (e.g., page size, start index).

Example:

const result = query('templates')
    .sort('createdAt', 'desc')
    .page(1)
    .pageSize(10)
    .get();

4. Population of Relations

Strapi allows for complex relationships between content types (e.g., one-to-many or many-to-many). With the populate method, you can easily fetch related content along with the main resource in a single query. The library also supports nested relations, meaning you can populate relations within relations, with filtering options to optimize your queries.

Example:

const result = query('books')
    .populate('author')
    .populate('author.friends');

Even populating with sorting and filtering is supported

Let’s take for example the more complex samples from the Strapi docs:

Here’s the QS generated query string:

GET /api/articles?populate[categories][sort][0]=name%3Aasc&populate[categories][filters][name][$eq]=Cars

It was created using the following piece of code:

const qs = require('qs');
const query = qs.stringify(
  {
    populate: {
      categories: {
        sort: ['name:asc'],
        filters: {
          name: {
            $eq: 'Cars',
          },
        },
      },
    },
  },
  {
    encodeValuesOnly: true, // prettify URL
  }
);

With our query builder it’s as simple as:

query("products")
  .populateQ("categories", (q) =>
    q.sort("name", "asc")
     .where("name.$eq", "Cars")
  );

5. Content Status & Localization

The ability to filter content by its status (e.g., published or draft) is crucial for content management. The Strapi Fluent Query Builder makes this easy with dedicated methods to retrieve content based on its publication state. Additionally, it supports querying content in different locales, which is useful for building multilingual applications.

Example:

const result = query('templates').published();  // Fetch only published content
const result = query('templates').locale('en'); // Query content in English

6. Field Selection

Another powerful feature is field selection. You can specify exactly which fields to retrieve from the API, reducing unnecessary data in the response and improving application performance. This is especially useful when working with large datasets.

Example:

const result = query('templates').fields('id', 'title', 'description');
// select is equivalent to fields, it just exists as a more descriptive name
const result = query('templates').select('id', 'title', 'description');

7. Transform your built query into anything you need

You can get the output of the query builder into the following:

  • To JSON by calling .json() when you’re done building, it will give you a stringified version of the query in JSON form.

  • As an object by calling .get(), this will return the fully built JavaScript object.

  • As a query string only by calling .qs(), this will only return the query string part of the URL.

  • As a full URL (path + query string) by calling .full().

8. Type Safety with TypeScript

The library is built with TypeScript to ensure full type safety across all query operations. This not only helps with autocompletion and error checking during development but also ensures that you’re working with well-structured data when building queries, even when populating nested relations.

While far from perfect, it tries to provide you with as good a guess as possible on your structure without getting in your way.

Why You Should Use It

Let’s go back and reiterate on what you would get by using the query builder instead of creating filters manually:

  • Cleaner Code: By chaining methods together, you can create complex queries with minimal boilerplate.

  • Easier Maintenance: The fluent API makes it easier to read and maintain queries, especially when working with dynamic or variable conditions.

  • Flexibility: Although optimized for Strapi, this library is agnostic and can be used with any RESTful API that follows similar conventions.

  • Unopinionated: It helps you cobble together the requests, nothing more, nothing less, bring your own way to make requests and go to town.

  • Very good coverage: I don’t have a percentage for you, but the vast majority of operations that are possible by creating a filter manually should be possible with this builder. Aiming at 100%, if you find an incompatibility, feel free to open an issue, or even better - a PR on the GitHub repository.

How to Get Started

Getting started with Strapi Fluent Query Builder is quick and easy. Simply install the package via npm or yarn:

npm install @codechem/strapi-fluent-rest-api

Or:

yarn add @codechem/strapi-fluent-rest-api

Once installed, you can start building queries like never before. Here’s a simple example:

import { query } from '@codechem/strapi-fluent-rest-api';

const result = query('books')
    .where('title.$contains', 'example')
    .sort('createdAt', 'desc')
    .page(1)
    .pageSize(10);

const myData = await request.get(result.full());

The library supports a wide range of features and can be customized to fit your needs.

You can find the repository on GitHub and the package itself on NPM.

Playground / CodeSandbox

You can play around with the query builder below and list some of the events on 42.mk to get a feel of the API and its capabilities, 42.mk runs a version of Strapi 4.

If you want to chat about the library, feel free to find me on the Discord server of 42.mk.

Conclusion

I hope the Fluent Query Builder can help make working with the Strapi REST API easier, cleaner, and more efficient.

Feel free to explore the library, contribute to its development, and let me know how it improves your workflow!

I’m open to feedback, contributions and ideas, shoot me a message, comment, issue or whatever else comes to your mind. :)

Cheers,

Ilija

371 views