Custom Controllers in Strapi
A controller in Strapi is a class that handles HTTP requests. By default, each content type gets standard controllers (find, findOne, create, update, delete). A custom controller overrides standard behavior or adds new endpoints.
Controller Structure
// src/api/article/controllers/article.ts
import { factories } from '@strapi/strapi'
export default factories.createCoreController('api::article.article', ({ strapi }) => ({
// Override find — add additional logic
async find(ctx) {
// Add view counter to response
const response = await super.find(ctx)
// Add meta information
response.meta.generatedAt = new Date().toISOString()
return response
},
// Override findOne — increment view counter
async findOne(ctx) {
const response = await super.findOne(ctx)
if (response.data) {
const { id } = ctx.params
// Update counter asynchronously (don't block 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() },
})
// Notify subscribers
await strapi.service('api::newsletter.newsletter').notifySubscribers(updated)
return this.transformResponse(updated)
},
}))
Route for Custom Action
// src/api/article/routes/article.ts
import { factories } from '@strapi/strapi'
export default factories.createCoreRouter('api::article.article', {
// Add 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 author automatically
ctx.request.body.data.author = ctx.state.user.id
return super.create(ctx)
}
Timeline
Developing custom controllers for 2–3 content types with additional endpoints and validation takes 2–3 days.







