Skip to content

Strapi API Customization — Custom Controllers, Routes, Services, and Middlewares

DodaTech Updated 2026-06-28 8 min read

In this tutorial, you will learn how to customize Strapi's auto-generated API by creating custom controllers, routes, services, and middlewares to add business logic, custom endpoints, and data transformation beyond the default CRUD operations.

What You'll Learn

  • The MVC architecture Strapi uses for API customization
  • How to create custom controllers with custom actions
  • How to add custom routes to existing content types
  • How to write services for reusable business logic
  • How to create custom middlewares for request processing
  • How to override default controller and service methods

Why It Matters

Auto-generated CRUD endpoints handle 80% of API needs. But real applications need custom logic — sending emails when content is created, transforming data before returning it, or adding endpoints that aggregate data across multiple types. API customization lets you build these features without leaving Strapi.

Real-World Use

A job board needs a custom endpoint that returns a summary of all job postings grouped by category, with counts of active, filled, and expired positions. This is not a simple CRUD operation. A custom controller method queries the database, groups the results, and returns a structured response that the frontend renders as a dashboard.

Learning Path

flowchart LR
  A["API Parameters"] --> B["GraphQL Setup"]
  B --> C["API Customization
-- You are here"]:::current C --> D["API Security"] D --> E["Users & Roles"] E --> F["Permissions"] classDef current fill:#4945ff,color:#fff,stroke-width:2px

The API Structure

Each content type in Strapi has its own API folder with controllers, services, routes, and policies:

src/api/article/
  controllers/
    article.js    -- Handle HTTP requests
  services/
    article.js    -- Business logic
  routes/
    article.js    -- URL to controller mapping
  policies/
    article.js    -- Access control (optional)
  content-types/
    article/
      schema.json -- Content type definition
      lifecycle.js -- Lifecycle hooks (optional)

Strapi auto-generates basic controllers, services, and routes for every content type. Your customization files are merged with the auto-generated ones.

Custom Controllers

Controllers handle HTTP requests and return responses. To customize the article controller:

// src/api/article/controllers/article.js
"use strict";

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

module.exports = createCoreController("api::article.article", ({ strapi }) => ({
  // Override the default find method
  async find(ctx) {
    const { data, meta } = await super.find(ctx);

    // Add custom logic: group articles by category
    const grouped = data.reduce((acc, article) => {
      const category = article.attributes.category?.data?.attributes?.name || "Uncategorized";
      if (!acc[category]) acc[category] = [];
      acc[category].push(article);
      return acc;
    }, {});

    return { grouped, meta };
  },

  // Custom action: get articles summary
  async summary(ctx) {
    const articles = await strapi.entityService.findMany("api::article.article", {
      populate: { category: true },
    });

    const summary = articles.reduce((acc, article) => {
      const catName = article.category?.name || "Uncategorized";
      acc[catName] = (acc[catName] || 0) + 1;
      return acc;
    }, {});

    return { summary };
  },
}));

The createCoreController factory wraps the default controller so you can call super to invoke default behavior and then extend it.

Custom Routes

Custom routes map URLs to controller actions. Create a route file to expose your custom controller methods:

// src/api/article/routes/article.js
"use strict";

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

module.exports = createCoreRouter("api::article.article", {
  config: {
    find: { auth: false },     // Make find public
    findOne: { auth: false },  // Make findOne public
  },
});

// Custom routes for custom controller actions
// src/api/article/routes/custom-article.js
module.exports = {
  routes: [
    {
      method: "GET",
      path: "/articles/summary",
      handler: "article.summary",
      config: {
        auth: false,
        policies: [],
        middlewares: [],
      },
    },
  ],
};

The first file overrides core route configuration. The second file adds a new route that maps to the summary custom action.

Custom Services

Services encapsulate business logic that can be reused by controllers, lifecycle hooks, and other services.

// src/api/article/services/article.js
"use strict";

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

module.exports = createCoreService("api::article.article", ({ strapi }) => ({
  // Custom service method
  async getArticleCountByCategory() {
    const articles = await super.find({
      populate: { category: true },
    });

    const counts = {};
    for (const article of articles.results) {
      const cat = article.category?.name || "Uncategorized";
      counts[cat] = (counts[cat] || 0) + 1;
    }

    return counts;
  },

  // Override default create to add custom logic
  async create(params) {
    // Add audit log before creating
    strapi.log.info(`Creating article: ${JSON.stringify(params.data)}`);

    // Call the default create
    const result = await super.create(params);

    // Send notification after creating
    await strapi.service("api::notification.notification").send({
      type: "article_created",
      articleId: result.id,
    });

    return result;
  },
}));

Services are called from controllers using strapi.service("api::article.article"). You can also call one service from another service.

Custom Middlewares

Middlewares Process requests before they reach the controller. They can modify the request, validate data, log activity, or block requests.

// src/middlewares/article-view-counter.js
module.exports = (config, { strapi }) => {
  return async (ctx, next) => {
    // Get the article ID from the URL
    const articleId = ctx.params.id;

    if (articleId) {
      // Increment view count in the database
      await strapi.db.query("api::article.article").update({
        where: { id: articleId },
        data: {
          views: strapi.db.query("api::article.article").raw("views + 1"),
        },
      });
    }

    // Continue to the next middleware or controller
    await next();
  };
};

Register the middleware in config/middlewares.js:

// config/middlewares.js
module.exports = [
  "strapi::logger",
  "strapi::errors",
  "strapi::security",
  "strapi::cors",
  "strapi::poweredBy",
  "strapi::query",
  "strapi::body",
  "strapi::session",
  "strapi::favicon",
  "strapi::public",
  {
    name: "global::article-view-counter",
    config: {},
  },
];

Middleware naming: strapi:: for built-in, global:: for project middleware, plugin:: for plugin middleware.

Custom Policies

Policies control access to specific routes or controllers. They return true (allow) or false (deny).

// src/policies/is-article-author.js
module.exports = (policyContext, config, { strapi }) => {
  const articleId = policyContext.params.id;
  const userId = policyContext.state.user?.id;

  if (!userId) return false;

  // Check if the current user is the author of the article
  return strapi.db.query("api::article.article").findOne({
    where: { id: articleId, author: userId },
  });
};

Apply policies in route configuration:

// routes/custom-article.js
{
  method: "PUT",
  path: "/articles/:id",
  handler: "article.update",
  config: {
    policies: ["is-article-author"],  // Only author can update
  },
}

Entity Service API

Strapi provides the Entity Service as a low-level API for working with content programmatically. Use it in services and controllers.

// Entity Service CRUD
// Find entries
const articles = await strapi.entityService.findMany("api::article.article", {
  filters: { title: { $containsi: "strapi" } },
  populate: { author: true },
  sort: { createdAt: "desc" },
  limit: 10,
});

// Find one entry
const article = await strapi.entityService.findOne("api::article.article", 1, {
  populate: ["author", "tags"],
});

// Create entry
const newArticle = await strapi.entityService.create("api::article.article", {
  data: {
    title: "New Article",
    content: "Content",
    author: 1,
  },
});

// Update entry
await strapi.entityService.update("api::article.article", 1, {
  data: { title: "Updated Title" },
});

// Delete entry
await strapi.entityService.delete("api::article.article", 1);

The Entity Service handles permissions, lifecycle hooks, and data validation automatically.

Common Mistakes

  1. Overriding instead of extending. Using createCoreController without calling super replaces the default behavior entirely. Always call super unless you intentionally want to replace the default.

  2. Putting business logic in controllers. Controllers should be thin — they handle HTTP and delegate to services. Business logic belongs in services for testability and reusability.

  3. Not registering custom routes. Creating a custom controller method without a corresponding route file means the endpoint does not exist. Always create a route for every custom action.

  4. Forgetting error handling. Custom controller methods can throw errors. Always wrap logic in try/catch and return appropriate HTTP status codes.

  5. Hardcoding content type IDs. Using hardcoded IDs in services makes the code fragile. Query by slugs or other identifiers instead.

Practice Questions

  1. What is the difference between a controller and a service in Strapi? Answer: Controllers handle HTTP requests and responses. Services contain business logic. Controllers call services. Services should not directly access HTTP context.

  2. How do you add a custom endpoint to an existing content type? Answer: Create a custom route file in src/api/{content-type}/routes/ that maps a URL path to a controller action, then implement the action in the controller file.

  3. When would you use a middleware instead of a policy? Answer: Use middleware for request/response processing (logging, Rate Limiting, data transformation) and policies for access control (checking user roles or ownership).

  4. Challenge: Build a complete custom API feature: (1) Create a custom endpoint POST /api/articles/:id/upvote that increments an upvote counter on the article, (2) Add a policy that prevents an author from upvoting their own article, (3) Create a service method that returns the top 10 most upvoted articles, (4) Add a custom route for the top articles endpoint.

FAQ

Can I use ES modules (import/export) in Strapi customization?

Strapi uses CommonJS (require/module.exports) by default. If you enable TypeScript, you can use ES module syntax. For JavaScript projects, stick with CommonJS for compatibility with Strapi's core.

How do I call one service from another service?

Use strapi.service('api::content-type.service-name') to access any registered service. For example, strapi.service('api::article.article').getArticleCount().

What happens if I delete a core controller file?

Deleting a core controller file causes Strapi to use its auto-generated default. Strapi never fails due to missing customization files. Each file is optional and overrides the default behavior.

Can I create routes that respond to multiple HTTP methods?

Yes. In the route configuration, set the method to an array: method: ['GET', 'POST']. The same handler can process multiple HTTP methods.

How do I access the current user in a custom controller?

The current authenticated user is available at ctx.state.user. For unauthenticated requests, this is null. For admin panel requests, user information is also available through the admin context.

Mini Project

Your task: Build a custom analytics API for your content.

  1. Create a custom service that calculates:
    • Total articles published per month for the last 6 months
    • Most popular categories by article count
    • Authors with the most published articles
    • Average time between draft creation and publication
  2. Create custom controller methods that expose this data through the API.
  3. Add custom routes for each analytics endpoint.
  4. Create a middleware that logs every API request to the analytics data.
  5. Test all endpoints and verify the response format.

What's Next

Now that you can customize the API, proceed to API Security to learn about CORS configuration, rate limiting, query complexity analysis, and input sanitization. After that, explore Users & Roles to set up authentication and authorization.

Related lessons:

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro