DEV Community

Cover image for Implement custom endpoint with pagination in Strapi
Bojan Stanojevic
Bojan Stanojevic

Posted on • Edited on

Implement custom endpoint with pagination in Strapi

Strapi has to be one of the most popular open source CMS platforms currently available. I used it in several projects and loved it's features and what you get for free. It's very easy to host on popular platforms like Digital Ocean or Netlify, very easy to use, and offers a bunch of features out of the box. In this article I will not go into details about how Strapi works and how to set it up, there are plenty of tutorials available online. What I would like to discuss is something I did not find online, and that is how you can customize Strapi endpoints to suit your needs.

Strapi Endpoints

Once you setup Strapi, from the admin dashboard (http://localhost:1337/admin) you can access Content Type Builder and create a new type. Type can be a collection, or a single type. Collection would be for example a collection of posts, or collection of reviews, companies, users etc.

Any type that you create will consist of default values (id, createdAt, updatedAt etc.) and values you define like name, category, city, country etc.

Once a type is created, you will have a new API endpoint. For example, if you created a hotels type, you will now have an endpoint https://localhost:1337/api/hotels that will list all the hotels that are created and published. With this new endpoint you will have access to HTTPS method like find, findOne, delete, and create. Access to these methods is configurable from the admin dashboard so for example find can be available for all the visitors, while delete method can only access authenticated roles.

All the options that are editable from the admin are, of course, accessible and editable from the Strapi project as well, for example to customize route /api/hotels you would go to src/api/hotels

When to customize Strapi?

While working with Strapi I can honestly say available features covered 90% use cases my clients had. But, depending on the project and stakeholder requirements I encountered situations where default features where not sufficient and I had to update how Strapi handles data in certain cases.

Let's continue with the example of hotels. Say you are building a directory of hotels and your stakeholder has this request:

For each location a hotel can belong to, I need to have the ability to control first 10 positions inside that location from the admin dashboard.

One way you can solve this request is to create a repeatable component inside Strapi where administrator can choose a location and a position for that location. Since this is a repeatable component, administrator can define positions for all required locations.

Strapi Repeatable Component

So when you hit that api endpoint this value will look like this:



"position": [
{
"id": 1,
"position": 1,
"locations": [
{
"id": 11,
"name": "Iowa",
"createdAt": "2024-03-03T20:01:28.602Z",
"updatedAt": "2024-04-23T12:37:27.654Z",
"publishedAt": "2024-03-03T20:01:29.526Z",
}
]
},
{
"id": 2,
"position": 3,
"locations": [
{
"id": 14,
"name": "California",
"createdAt": "2024-03-03T20:01:28.602Z",
"updatedAt": "2024-04-23T12:37:27.654Z",
"publishedAt": "2024-03-03T20:01:29.526Z",
}
]
}
],


Enter fullscreen mode Exit fullscreen mode

How to customize an API route

Having this kind of data structure will work to administer first 10 positions for different locations, but you will not be able to apply this type of sorting in Strapi by default, so we will have to do some customization from the Strapi backend.

In your Strapi project navigate to src/api/hotels and you will see a couple of folders there. The one we are looking for is the controllers folder, we need to edit the file inside that folder.

Controllers are JavaScript files that contain a set of methods, called actions, reached by the client according to the requested route. Whenever a client requests the route, the action performs the business logic code and sends back the response.

You can read more about controllers on the official Strapi docs, but essentially editing controllers will allow us to structure the data the way we need it in order to apply custom sorting of the hotels.

We will be editing the default find method inside the hotels controller, to start your controller file will look like this:



"use strict";

const { createCoreController } = require("@strapi/strapi").factories;

module.exports = createCoreController("api::hotel.hotel", ({ strapi }) => ({
  async find(ctx, next) {

  },
}));



Enter fullscreen mode Exit fullscreen mode

Next, inside the find function we'll extract the query sort_by_premium_position and save it in a variable premiumSort. So only in case sort_by_premium_position query param is present containing the location id in the request we will apply the logic below.

We will also define a helper function parseQueryParam. This function is designed to handle and parse query parameters that may come in different formats. It processes the input parameter param based on its type and returns a parsed version of it.



"use strict";

const { createCoreController } = require("@strapi/strapi").factories;

module.exports = createCoreController("api::hotel.hotel", ({ strapi }) => ({
  async find(ctx, next) {
    const { query } = ctx;
    const premiumSort = ctx.query.sort_by_premium_position;

    // Utility function to parse query parameters
    const parseQueryParam = (param) => {
      if (Array.isArray(param)) {
        return param.map((p) => JSON.parse(p));
      }
      if (typeof param === "string") {
        return JSON.parse(param);
      }
      return param;
    };
  },
}));



Enter fullscreen mode Exit fullscreen mode

Finally, if premiumSort is true we will get all the hotels, apply custom sorting and build custom pagination while making sure all query parameters like filtering, pagination and populating still works. If premiumSort is not present we will not change anything and just return the data normally.

Here is the entire code inside the controller file:



"use strict";

const { createCoreController } = require("@strapi/strapi").factories;

module.exports = createCoreController("api::hotel.hotel", ({ strapi }) => ({
async find(ctx, next) {
const { query } = ctx;
const premiumSort = ctx.query.sort_by_premium_position;

// Utility function to parse query parameters
const parseQueryParam = (param) => {
  if (Array.isArray(param)) {
    return param.map((p) => JSON.parse(p));
  }
  if (typeof param === "string") {
    return JSON.parse(param);
  }
  return param;
};
Enter fullscreen mode Exit fullscreen mode

try {
// Check if sorting by location is requested, if not we return everything regularly
if (premiumSort) {
const locationToSortBy = Array.isArray(premiumSort)
? premiumSort[0]
: premiumSort;

// Extract and parse query parameters, with fallbacks for undefined values
const filters = query.filters ? parseQueryParam(query.filters) : {};
const sort = query.sort ? parseQueryParam(query.sort) : [];
let populate = [];

if (query.populate) {
  if (Array.isArray(query.populate)) {
    populate = query.populate;
  } else if (typeof query.populate === "string") {
    populate = query.populate.split(",");
  }
}

// Here we parse pagination parameters, supporting both string and array formats
let pagination = query.pagination ? parseQueryParam(query.pagination) : {};
if (typeof pagination === "string") {
  pagination = JSON.parse(pagination);
} else if (Array.isArray(pagination)) {
  pagination = pagination.reduce(
    (acc, p) => ({ ...acc, ...JSON.parse(p) }),
    {}
  );
}

// Default pagination values if not specified
const page = pagination.page ? parseInt(pagination.page, 10) : 1;
const pageSize = pagination.pageSize ? parseInt(pagination.pageSize, 10) : 10;

// Fetch all data without pagination to apply custom sorting logic
const entities = await strapi.entityService.findMany(
  "api::hotel.hotel",
  {
    filters,
    sort,
    populate: ["position", "position.locations", ...populate],
  }
);

// Filter and sort entities based on the specified location ID and position
let sortedEntities = entities.sort((a, b) => {
  const aPos = a.position.find((p) =>
    p.locations.some((c) => c.id === parseInt(locationToSortBy))
  );
  const bPos = b.position.find((p) =>
    p.locations.some((c) => c.id === parseInt(locationToSortBy))
  );

  // Sort by position values within the specified location
  if (aPos && bPos) {
    return aPos.position - bPos.position;
  } else if (aPos) {
    return -1;
  } else if (bPos) {
    return 1;
  }
  return 0;
});

// Calculate pagination metadata
const total = sortedEntities.length;
const pageCount = Math.ceil(total / pageSize);
const paginatedEntities = sortedEntities.slice(
  (page - 1) * pageSize,
  page * pageSize
);

// Construct metadata for pagination response
const meta = {
  pagination: {
    page,
    pageSize,
    pageCount,
    total,
  },
};

// Return sorted and paginated entities along with pagination metadata
return { data: paginatedEntities, meta };
Enter fullscreen mode Exit fullscreen mode

} else {
// If no custom sorting is requested, return default find method
const { data, meta } = await super.find(ctx);
return { data: data, meta };
}
} catch (error) {
console.error("Error in find function:", error);
ctx.throw(400, "Invalid query parameters");
}
},
}));

Enter fullscreen mode Exit fullscreen mode




Conclusion

And that's it! Obviously, this example is related to a specific use case, but hopefully this example may help anyone who encounters similar requests while working on Strapi projects.

To work with me, visit my website.

If you enjoyed this post I'd love it if you could give me a follow on Twitter by clicking on the button below! :)
Dellboyan Twitter

Top comments (0)