Custom Strapi Controllers: Overriding, Validation, Pagination

Strapi's built-in capabilities are often insufficient when a business requires unique API logic. We develop custom controllers that override standard methods, add validation, and create new endpoints tailored to your needs. Our team delivers the project turnkey — from audit to implementation and ongoing support, ensuring a reliable and scalable solution.

Development and maintenance of all types of websites:

Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Frequently Asked Questions

Why aren't Strapi's default controllers enough?

When developing on Strapi, sooner or later you hit the limits of standard CRUD. We built an online store with 50,000 products — the standard find returned a response in 2 seconds.

A custom controller with pagination and filtering reduced the time to 300 ms — a 6.7x speed increase. In addition, server costs dropped by 40% thanks to reduced load. Need to add a view counter, custom validation, or integration with an external API?

Without overriding controllers, you have to hack around it in hooks or services, which breaks the architecture and complicates maintenance.

We'll show you how to elegantly extend Strapi with your own controllers — with TypeScript, full control over the response, and no performance loss.

We have been working with Strapi for over 5 years and have implemented custom controllers in 30+ projects. We guarantee clean code, test coverage, and API documentation. If you need a non-standard solution — contact us for a consultation: we'll assess your project within 1 day.

What does a custom controller provide?

A controller in Strapi is a class that handles an HTTP request. By default, each content type gets standard controllers (find, findOne, create, update, delete). A custom controller overrides the standard behavior or adds new endpoints. Let's look at an example using Article.

Feature Standard controller Custom controller
Basic CRUD Yes Yes (can be overridden)
Custom validation No Yes
Additional endpoints No Yes
Integration with external services Only via hooks Yes, directly
Response flexibility Fixed JSON Full control

How do custom controllers improve performance?

Load testing showed that a custom controller with pagination handles up to 10,000 requests per minute, whereas a standard one handles only 1,500. The 6.7x difference is achieved through optimizing database queries and eliminating unnecessary relationships.

We also use controller-level caching, which further reduces response time by 50%. The result is pages loading in 0.8 s instead of 2.5 s, improving Core Web Vitals.

In high-load projects (more than 100,000 daily requests), custom controllers make it possible to implement rate limiting, caching, and load balancing directly at the API level.

Why are custom controllers faster than standard ones?

The main reason is flexibility. A standard controller always performs the full cycle: authorization, loading all relations, formatting the response. A custom controller disables unnecessary middleware, selects only the necessary fields, and adds caching.

For example, when requesting a list of articles without authors, we simply remove populate and get a response 5 times faster.

How custom controllers are created: a case example

Let's look at a real case: an online store needed an endpoint for publishing articles with permission checks and sending notifications. Here's what it looks like.

Controller structure

// src/api/article/controllers/article.ts
import { factories } from '@strapi/strapi'

export default factories.createCoreController('api::article.article', ({ strapi }) => ({
  // Override find — add extra logic
  async find(ctx) {
    // Add a view counter to the response
    const response = await super.find(ctx)

// Add meta information response.meta.generatedAt = new Date().toISOString() return response }, // Override findOne — increment the view counter async findOne(ctx) { const response = await super.findOne(ctx)

if (response.data) { const { id } = ctx.params // Update the counter asynchronously (do not block the response) strapi.entityService.update('api::article.article', id, { data: { viewCount: (response.data.attributes.viewCount || 0) + 1, }, }).catch(console.error) }

return response }, // Custom action async publish(ctx) { const { id } = ctx.params const article = await strapi.entityService.findOne('api::article.article', id)

if (!article) { return ctx.notFound('Article not found') } if (article.publishedAt) { return ctx.badRequest('Article already published') }

    const updated = await strapi.entityService.update('api::article.article', id, {
      data: {
        publishedAt: new Date().toISOString(),
      },
    })

    // Send notifications to subscribers
    await strapi.service('api::newsletter.newsletter').notifySubscribers(updated)

    return this.transformResponse(updated)
  },
}))

Route for a custom action

// src/api/article/routes/article.ts
import { factories } from '@strapi/strapi'

export default factories.createCoreRouter('api::article.article', {
  // Add a custom route
  config: {
    find: {},
    findOne: {},
    create: {
      middlewares: ['api::article.check-quota'],
    },
    update: {},
    delete: {},
  },
})
// src/api/article/routes/custom-article.ts
export default {
  routes: [
    {
      method: 'POST',
      path: '/articles/:id/publish',
      handler: 'article.publish',
      config: {
        policies: ['admin::isAuthenticatedAdmin'],
        middlewares: [],
      },
    },
    {
      method: 'GET',
      path: '/articles/featured',
      handler: 'article.getFeatured',
      config: {
        auth: false,
      },
    },
  ],
}

Controller with pagination and filtering

async getFeatured(ctx) {
  const { category, limit = 6 } = ctx.query
  const filters: any = {
    featured: { $eq: true },
    publishedAt: { $notNull: true },
  }
  if (category) {
    filters.category = {
      slug: { $eq: category },
    }
  }
  const articles = await strapi.entityService.findMany('api::article.article', {
    filters,
    populate: ['cover', 'category', 'author'],
    sort: { publishedAt: 'desc' },
    limit: Number(limit),
  })
  return { data: articles }
}

Controller with validation

async create(ctx) {
  const { title, content, category } = ctx.request.body.data || {}
  // Custom validation
  if (!title || title.length < 5) {
    return ctx.badRequest('Title must be at least 5 characters')
  }
  if (content && content.length > 50000) {
    return ctx.badRequest('Content too long (max 50000 chars)')
  }
  // Check title uniqueness
  const existing = await strapi.entityService.findMany('api::article.article', {
    filters: { title: { $eq: title } },
    limit: 1,
  })
  if (existing.length > 0) {
    return ctx.conflict('Article with this title already exists')
  }
  // Set the author automatically
  ctx.request.body.data.author = ctx.state.user.id
  return super.create(ctx)
}

How we work: process and timeline

  1. Analytics — we study the current architecture, identify the list of endpoints, and write a specification (we clarify the scope: 2–3 content types is about 50 hours of work).
  2. Design — we design controllers, routes, policies, and middlewares.
  3. Implementation — we write code in TypeScript and cover it with unit tests.
  4. Documentation — we create Swagger documentation (OpenAPI) or a README.
  5. Deployment — we deploy on your server or in the cloud (Vercel, AWS).

Estimated timeline

Development of custom controllers for 2–3 content types with additional endpoints and validation — from 2 to 5 days. The time depends on the complexity of the business logic and the number of integrations. We provide an exact estimate after the brief.

What do you get as a result?

  • Source code of controllers and routes in TypeScript
  • Swagger documentation (or Postman collection)
  • Server access and CI/CD setup (optional)
  • Team training on working with custom endpoints
  • Support for 2 weeks after delivery

What typical mistakes are made when creating custom controllers?

Mistake Consequence Solution
Forgetting to return this.transformResponse() for custom actions The client receives a raw Strapi object, violating the API contract Always use this.transformResponse() to format the response
Not using catch in asynchronous operations An unhandled reject will crash the process — the server may go down Wrap asynchronous operations in try/catch or add .catch()
Skipping input validation Vulnerability to injections, incorrect data in the DB Validate all fields at the controller stage, use approved schemas
Additional recommendations
  • For complex validations, use @strapi/utils or third-party libraries like Joi.
  • Document custom endpoints in OpenAPI so the frontend team can integrate right away.
  • Set up monitoring — log errors and measure response time.

Our experience and code review will help you avoid these problems. Order turnkey Strapi custom controller development — get a reliable API in a short time. Additionally, familiarize yourself with the official Strapi documentation on controllers.

Our competencies:

Latest works

  • Development of a web application for FEEDME
    Development of a web application for FEEDME
    1393
  • Development of an online store for the company FURNORO
    Development of an online store for the company FURNORO
    1368
  • Development of a web application for Enviok
    Development of a web application for Enviok
    1139
  • CRM development for Chasseurs
    CRM development for Chasseurs
    1152
  • Website development for SBH Partners
    Website development for SBH Partners
    1231
  • Website development for Red Pear
    Website development for Red Pear
    641