Strapi API Customization — Custom Controllers, Routes, Services, and Middlewares
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
Overriding instead of extending. Using
createCoreControllerwithout callingsuperreplaces the default behavior entirely. Always callsuperunless you intentionally want to replace the default.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.
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.
Forgetting error handling. Custom controller methods can throw errors. Always wrap logic in try/catch and return appropriate HTTP status codes.
Hardcoding content type IDs. Using hardcoded IDs in services makes the code fragile. Query by slugs or other identifiers instead.
Practice Questions
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.
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.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).
Challenge: Build a complete custom API feature: (1) Create a custom endpoint
POST /api/articles/:id/upvotethat 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
Mini Project
Your task: Build a custom analytics API for your content.
- 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
- Create custom controller methods that expose this data through the API.
- Add custom routes for each analytics endpoint.
- Create a middleware that logs every API request to the analytics data.
- 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:
- REST API Design — Custom endpoints best practices
- Node.js — How Koa processes requests
- GraphQL — Customizing Graphql resolvers
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro