How to Customize the Strapi Back-end (Services, Routes, Controllers, Queries) using TypeScript

Every back-end service in existence has some form of system in place that enables them to receive requests and act on them appropriately (providing some response). Services, routes, and controllers are all represented in such servers. Strapi also has this system in place and offers ways for you to customize them to your preferences, such as adding capabilities or developing new ones.

This is a companion discussion topic for the original entry at

Great guide, Alex!

I’m wondering - when I create a custom controller and use the entity service - the return format is not the same as when you use Strapi’s default controllers. You know, by default you get your return object wrapped in data- and attributes-fields. When using entity service, you get a more “flat” return object.

Do you know if there’s a way to convert this flatter object to a format like { data: { attributes: … }, meta: } (like the default controllers)?

1 Like

Thanks for the tutorial!
I am usig strapi v4 and i get a type error on the

data: content_schemas.GetAttributesValues<"api::article.article">;
... =;

The error says:
Property ‘publishedAt’ does not exist on type ‘GetAttributesValues<“api::article.article”>’.ts(2339)

Can you help me out?

Thank You

Hey, this is a very relevant question. We are facing the same issue :slight_smile: : we have flat objects in our custom routes.

Did you find a satisfying solution?

When using entityService it will return just the flat structure. You would need to use transformResponse to transform the structure.


Docs Reference

  async find(ctx) {
      // validateQuery throws an error if any of the query params used are inaccessible to ctx.user
      // That is, trying to access private fields, fields they don't have permission for, wrong data type, etc
      await this.validateQuery(ctx);

      // sanitizeQuery silently removes any query params that are invalid or the user does not have access to
      // It is recommended to use sanitizeQuery even if validateQuery is used, as validateQuery allows
      // a number of non-security-related cases such as empty objects in string fields to pass, while sanitizeQuery
      // will remove them completely
      const sanitizedQueryParams = await this.sanitizeQuery(ctx);

      // Perform whatever custom actions are needed
      const { results, pagination } = await strapi

      // sanitizeOutput removes any data that was returned by our query that the ctx.user should not have access to
      const sanitizedResults = await this.sanitizeOutput(results, ctx);

      // transformResponse correctly formats the data and meta fields of your results to return to the API
      return this.transformResponse(sanitizedResults, { pagination });

You can also use super when updating a controller based an existing controller to extend it.

   async find(ctx) {
      // your custom logic for modifying the input
      ctx.query = { ...ctx.query, locale: "en" }; // force ctx.query.locale to 'en' regardless of what was requested

      // Call the default parent controller action
      const result = await super.find(ctx);

      // your custom logic for modifying the output =; // change the date that is returned

      return result;