Custom Middleware in Strapi
Middleware in Strapi are functions executed before or after request processing by the controller. They work like Express/Koa middleware. They are applied globally (to all routes) or to specific routes.
Types of Middleware
Strapi distinguishes:
-
Global middleware — applied to all requests, registered in
config/middlewares.js - Route middleware — applied to specific routes, registered in route configuration
Global Middleware — Logging
// src/middlewares/request-logger.ts
export default (config: any, { strapi }: any) => {
return async (ctx: any, next: any) => {
const start = Date.now()
await next()
const duration = Date.now() - start
const { method, url, status } = ctx
if (duration > 1000) {
strapi.log.warn(`Slow request: ${method} ${url} — ${duration}ms (${status})`)
}
strapi.log.debug(`${method} ${url} — ${duration}ms [${status}]`)
}
}
// config/middlewares.js — register globally
module.exports = [
'strapi::logger',
'strapi::errors',
'strapi::security',
'strapi::cors',
'global::request-logger', // custom middleware
'strapi::query',
'strapi::body',
'strapi::session',
'strapi::favicon',
'strapi::public',
]
Global Middleware — Rate Limiting
// src/middlewares/rate-limit.ts
const requests = new Map<string, number[]>()
export default (config: any) => {
const { maxRequests = 100, windowMs = 60_000 } = config
return async (ctx: any, next: any) => {
const ip = ctx.request.ip
const now = Date.now()
const windowStart = now - windowMs
// Clean up old requests
const timestamps = (requests.get(ip) || []).filter(t => t > windowStart)
if (timestamps.length >= maxRequests) {
ctx.status = 429
ctx.body = { error: 'Too Many Requests' }
ctx.set('Retry-After', String(Math.ceil(windowMs / 1000)))
return
}
timestamps.push(now)
requests.set(ip, timestamps)
await next()
}
}
// config/middlewares.js
{
name: 'global::rate-limit',
config: { maxRequests: 100, windowMs: 60000 }
}
Route Middleware — Subscription Check
// src/middlewares/check-subscription.ts
export default (config: any, { strapi }: any) => {
return async (ctx: any, next: any) => {
const userId = ctx.state.user?.id
if (!userId) {
ctx.unauthorized('Authentication required')
return
}
const user = await strapi.entityService.findOne(
'plugin::users-permissions.user',
userId,
{ populate: ['subscription'] }
)
if (!user?.subscription?.active) {
ctx.forbidden('Active subscription required')
return
}
await next()
}
}
// src/api/premium-content/routes/premium-content.ts
export default {
routes: [
{
method: 'GET',
path: '/premium-content',
handler: 'premium-content.find',
config: {
middlewares: ['api::premium-content.check-subscription'],
},
},
],
}
Middleware for Response Transformation
// src/middlewares/add-computed-fields.ts
export default () => {
return async (ctx: any, next: any) => {
await next()
// Add fields to API response after processing
if (ctx.url.startsWith('/api/products') && ctx.body?.data) {
const transform = (item: any) => ({
...item,
attributes: {
...item.attributes,
// Calculate discount
discountPercent: item.attributes.originalPrice
? Math.round((1 - item.attributes.price / item.attributes.originalPrice) * 100)
: 0,
},
})
if (Array.isArray(ctx.body.data)) {
ctx.body.data = ctx.body.data.map(transform)
} else {
ctx.body.data = transform(ctx.body.data)
}
}
}
}
Middleware for Multilingual URLs
// src/middlewares/locale-redirect.ts
const localeMap: Record<string, string> = {
'ru-RU': 'ru',
'en-US': 'en',
'uk-UA': 'uk',
}
export default () => {
return async (ctx: any, next: any) => {
// If Accept-Language header has locale, add to query
if (!ctx.query.locale) {
const acceptLang = ctx.get('Accept-Language')?.split(',')[0] || 'ru'
const locale = localeMap[acceptLang] || acceptLang.split('-')[0] || 'ru'
ctx.query.locale = locale
}
await next()
}
}
Timeline
Developing a set of middleware (rate limiting, logging, response transformation) takes 1–2 days.







