KINTO Tech Blog
Development

Implementing a custom API in Strapi

Cover Image for Implementing a custom API in Strapi

Introduction

Hello, I am Keyuno and I am part of the KINTO FACTORY front end development team.

As part of our KINTO FACTORY service, we are launching a dedicated magazine using Strapi, a headless content management system (CMS). *More details will be shared in an upcoming article, so please stay tuned!

In this article, I would like to explain how to add custom APIs to Strapi, which we implemented when introducing Strapi. This article covers the following two patterns of custom API implementation.

Optimizing web page management is a constant challenge. I hope this article helps ease the burden for engineers, even if just a bit.

Development Environment Details

Strapi version : Strapi 4
node version   : v20.11.0

Implementing a new custom API

This section shows how to implement a new custom API. While this approach offers high flexibility because it can be implemented at the SQL level, overdoing it can make maintenance difficult, so use it wisely.

1. Create a router

First, add the routes for the API endpoints you create.

Under src/api, there is a directory for each collectionType. In the figure below, the routes directory is under post. Create a file under routes for defining custom-route.

*According to the official documentation, there is a command npx strapi generat that prepares the necessary files (though I haven’t used it).

The file name can be anything

In the created file, write the following code:

custom.ts
export default {
  routes: [
    {
      method: "GET", // Refers to the HTTP method. Please modify as needed to suit your purposes.
      path: "/posts/customapi/:value", // These are the endpoints for the APIs you will implement.
      handler: "post.customapi", // Specify the controller that this route refers to.
    }
};

method

Specify the HTTP method. Please modify as needed to suit the API you are creating.

path

Specify the endpoint for the custom API you are implementing. The sample endpoint, /:value indicates that the trailing value is received as the value variable. For example, if /posts/customapi/1 and /posts/customapi/2 are called, the value will store 1 and 2 respectively.

handler

Specify the controller (explained later) that the custom API you are implementing refers to. Specify the name of the function in the controller that you want to reference.

2. Implement the controller

Implement the controller referenced by the routes implemented in step 1. Open the post.ts file located in the controllers directory , which is in the same level as the routes directory.

In this file, add the handler ( customapi ) specified in the previous routes to the default controller (CoreController) as follows:

Before change (initial state)

post.ts
import { factories } from '@strapi/strapi';

export default factories.createCoreController('api::post.post');

After change

post.ts
import { factories } from "@strapi/strapi";

export default factories.createCoreController("api::post.post", ({ strapi }) => ({
  async customapi(ctx) {
    try {
      await this.validateQuery(ctx);
      const entity = await strapi.service("api::post.post").customapi(ctx);
      const sanitizedEntity = await this.sanitizeOutput(entity, ctx);
      return this.transformResponse(sanitizedEntity);
    } catch (err) {
      ctx.body = err;
    }
  },
}));

What’s changed

  • Added a custom handler customapi() to the default controller
  • Retrieved the result of executing the customapi () service that contains the business logic customapi(), as referenced in line 8.

For details on validateQuery(), sanitizeOutput(), transformResponse() , please refer to Strapi’s official documentation .

3. Implement the service

Implement the service referenced by the controller implemented in step 2. Open the post.ts in the services directory , which is at the same level as the controllers directory.

Add the method (customapi) specified in the previous controller to the default service (CoreService) as shown below.

Before change (initial state)

post.ts
import { factories } from '@strapi/strapi';

export default factories.createCoreService('api::post.post');

After change

post.ts
import { factories } from "@strapi/strapi";

export default factories.createCoreService("api::post.post", ({ strapi }) => ({
  async customapi(ctx) {
    try {
      const queryParameter: { storeCode: string[]; userName: string } = ctx.query;
      const { parameterValue } = ctx.params;
      const sql = "/** Database to use, SQL according to purpose */";
      const [allEntries] = await strapi.db.connection.raw(sql);
      return allEntries;
    } catch (err) {
      return err;
    }
  },
}));

What’s changed

  • Add the custom service customapi() to the default service
  • Line 6: Retrieve the query parameter information
  • Line 7: Obtain endpoint parameter information
  • Line 10: Get the SQL execution results

4. Confirm operation

With this, the implementation of the new custom API is complete. Please actually try calling the API and check that it works as expected.

Overriding the default API

In this section, I will show an example of how to override the default entry detail retrieval API to allow fetching with a custom parameter.

[Entry detail retrieval API]

  • [Before override] GET /{collectionType}/:postId(number)
  • [After override] GET /{collectionType}/:contentId(string)

1. Create a router

It is basically the same as when implementing a new custom API. Add the following code to the custom.ts under the routes directory:

custom.ts
export default {
  routes: [
    {
      method: "GET",
      path: "/posts/:contentId",
      handler: "post.findOne",
    }
};

With this route addition, the endpoint that previously retrieved entry details using /posts/:postId(number) will now retrieve entry details using /posts/:contentId(string) (entry details can no longer be retrieved using /posts/:postId(number)).

2. Implement the controller

The implementation of the controller is basically the same as when implementing a new custom API. Modify the post.ts in the controllers directory, which is at the same level as the routes directory, as follows:

Before change (initial state)

post.ts
import { factories } from '@strapi/strapi';

export default factories.createCoreController('api::post.post');

After change

post.ts
import { factories } from "@strapi/strapi";
import getPopulateQueryValue from "../../utils/getPopulateQueryValue";

export default factories.createCoreController("api::post.post", ({ strapi }) => ({
  async findOne(ctx) {
    await this.validateQuery(ctx);
    const { contentId } = ctx.params;
    const { populate } = ctx.query;
    const entity = await strapi.query("api::post.post").findOne({
      where: { contentID: contentId },
      ...(populate && {
        populate: getPopulateQueryValue(populate),
      }),
    });
    const sanitizedEntity = await this.sanitizeOutput(entity, ctx);
    return this.transformResponse(sanitizedEntity);
  },
}));

What’s changed

  • Added a custom findOne() controller to the default controller
  • In line 12, it extracts records where the contentID column matches contentId.
    • Since .findOne() is used in line 11, the result will be a single object.

In this section, the business logic is implemented in the controller rather than the service.

3. Confirm operation

With this, the implementation to override the default API is complete. Please actually try calling the API and check that it works as expected.

Conclusion

This concludes the explanation of implementing custom API in Strapi.

I think Strapi is a highly customizable and great tool. Therefore, I hope to continue sharing my knowledge, and I would be happy if you could share your insights as well.

We also have other topics, such as:

  • Automatically building applications when publishing Strapi articles.
  • Embedding videos (e.g., mp4) in CKEditor.

I will cover these topics in future articles.

Thank you for reading.

Facebook

関連記事 | Related Posts

We are hiring!

【フロントエンドエンジニア(コンテンツ開発)】新車サブスク開発G/東京

新車サブスク開発グループについてTOYOTAのクルマのサブスクリプションサービスである『 KINTO ONE 』のWebサイトの開発、運用をしています。​業務内容トヨタグループの金融、モビリティサービスの内製開発組織である同社にて、自社サービスである、クルマのサブスクリプションサービス『KINTO ONE』のWebサイトコンテンツの開発・運用業務を担っていただきます。

【バックエンドエンジニア】my route開発G/東京

my route開発グループについてmy route開発グループは、my routeに関わる開発・運用に取り組んでいます。my routeの概要 my routeは、移動需要を創出するために「魅力ある地域情報の発信」、「最適な移動手段の提案」、「交通機関や施設利用のスムーズな予約・決済」をワンストップで提供する、スマートフォン向けマルチモーダルモビリティサービスです。