Strapi Custom Middleware — Request/Response Middleware, Order, and Config
In this tutorial, you will learn how to create custom middleware in Strapi — intercepting requests before they reach controllers, processing responses before they are sent, controlling middleware execution order, and configuring middleware for specific routes.
What You'll Learn
- What middleware is and how it fits into Strapi's request lifecycle
- How to create custom middleware for request preprocessing
- How to create custom middleware for response transformation
- How middleware execution order works in Koa's onion model
- How to configure middleware for specific routes only
- How to pass configuration to middleware
Why It Matters
Middleware lets you add cross-cutting concerns without modifying individual controllers. Authentication, logging, rate limiting, CORS, request validation — these are all middleware concerns. Instead of adding logging code to every controller, you write a logging middleware once and apply it to all routes or specific ones.
Real-World Use
An API needs to: log every request with timing information, add security headers to all responses, block requests from banned IP addresses, transform response formats for legacy clients, and add request IDs for tracing. All of these are implemented as middleware layers that Process every request or specific routes without touching controller code.
Learning Path
flowchart LR A["Lifecycle Hooks"] --> B["Custom Middleware
-- You are here"]:::current B --> C["Webhooks"] C --> D["Strapi TypeScript"] D --> E["Testing"] classDef current fill:#4945ff,color:#fff,stroke-width:2px
What is Middleware?
In Strapi (built on Koa), middleware functions form a stack. Each function receives the request context (ctx) and a next function that passes control to the next middleware in the stack.
Request flow through middleware:
Client Request
-> Middleware 1 (logging)
-> Middleware 2 (CORS)
-> Middleware 3 (body parser)
-> Router -> Controller -> Service
<- Middleware 3 (response headers)
<- Middleware 2 (response logging)
<- Middleware 1 (response timing)
Client Response
The Koa onion model means middleware runs twice — once when the request comes in and once when the response goes out.
Creating a Simple Middleware
// src/middlewares/request-timer.js
module.exports = (config, { strapi }) => {
return async (ctx, next) => {
const start = Date.now();
// Log incoming request
strapi.log.info(`${ctx.method} ${ctx.url}`);
// Pass control to the next middleware
await next();
// Calculate duration after other middleware/controller runs
const duration = Date.now() - start;
// Log response info
strapi.log.info(`${ctx.method} ${ctx.url} -> ${ctx.status} (${duration}ms)`);
// Add timing header to response
ctx.set("X-Response-Time", `${duration}ms`);
};
};
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::request-timer",
config: {}, // Config passed to the middleware function
},
];
The middleware name format matters:
strapi::— built-in Strapi middlewareglobal::— project middleware (insrc/middlewares/)plugin::— plugin middleware
Middleware with Configuration
Create middleware that accepts configuration:
// src/middlewares/rate-limiter.js
module.exports = (config, { strapi }) => {
const {
interval = 60000, // 1 minute
maxRequests = 100, // 100 requests per interval
message = "Too many requests, please try again later.",
} = config;
// In-memory request tracker
const requests = new Map();
// Cleanup old entries periodically
setInterval(() => {
const now = Date.now();
for (const [key, data] of requests.entries()) {
if (now - data.resetTime > interval) {
requests.delete(key);
}
}
}, interval);
return async (ctx, next) => {
const ip = ctx.ip;
const now = Date.now();
let entry = requests.get(ip);
if (!entry || now - entry.resetTime > interval) {
entry = { count: 0, resetTime: now + interval };
requests.set(ip, entry);
}
entry.count++;
if (entry.count > maxRequests) {
ctx.set("Retry-After", Math.ceil((entry.resetTime - now) / 1000).toString());
ctx.status = 429;
ctx.body = { error: { message, status: 429 } };
return; // Block request — do not call next()
}
// Add rate limit headers
ctx.set("X-RateLimit-Limit", maxRequests.toString());
ctx.set("X-RateLimit-Remaining", (maxRequests - entry.count).toString());
ctx.set("X-RateLimit-Reset", Math.ceil(entry.resetTime / 1000).toString());
await next();
};
};
Configure with custom options:
// config/middlewares.js
{
name: "global::rate-limiter",
config: {
interval: 30000, // 30 seconds
maxRequests: 50, // 50 requests per interval
message: "Slow down! Too many requests.",
},
}
Route-Specific Middleware
Some middleware should only apply to specific routes:
// src/middlewares/route-logger.js
module.exports = (config, { strapi }) => {
const { includePaths = ["/api/"], excludePaths = ["/api/health"] } = config;
return async (ctx, next) => {
// Only apply to included paths
const shouldApply = includePaths.some((path) => ctx.url.startsWith(path));
const shouldExclude = excludePaths.some((path) => ctx.url.startsWith(path));
if (!shouldApply || shouldExclude) {
await next();
return;
}
const start = Date.now();
strapi.log.info(`[Route Logger] ${ctx.method} ${ctx.url}`);
await next();
const duration = Date.now() - start;
strapi.log.info(`[Route Logger] ${ctx.method} ${ctx.url} -> ${ctx.status} (${duration}ms)`);
};
};
Or apply middleware per-route in the route configuration:
// src/api/article/routes/article.js
module.exports = {
routes: [
{
method: "GET",
path: "/articles",
handler: "article.find",
config: {
middlewares: ["global::response-formatter"], // Route-specific middleware
},
},
],
};
Common Middleware Use Cases
// src/middlewares/security-headers.js
module.exports = (config, { strapi }) => {
return async (ctx, next) => {
// Add security headers to every response
ctx.set("X-Content-Type-Options", "nosniff");
ctx.set("X-Frame-Options", "DENY");
ctx.set("X-XSS-Protection", "1; mode=block");
ctx.set("Referrer-Policy", "strict-origin-when-cross-origin");
ctx.set("Permissions-Policy", "geolocation=(), microphone=(), camera=()");
await next();
};
};
// src/middlewares/request-id.js
const { v4: uuidv4 } = require("uuid");
module.exports = (config, { strapi }) => {
return async (ctx, next) => {
const requestId = ctx.request.headers["x-request-id"] || uuidv4();
ctx.request.requestId = requestId;
ctx.set("X-Request-ID", requestId);
await next();
};
};
// src/middlewares/maintenance-mode.js
module.exports = (config, { strapi }) => {
return async (ctx, next) => {
if (process.env.MAINTENANCE_MODE === "true") {
ctx.status = 503;
ctx.body = {
error: {
message: "Site is under maintenance. Please check back later.",
status: 503,
},
};
return; // Block all requests
}
await next();
};
};
Middleware Execution Order
The order in config/middlewares.js determines the execution order:
// config/middlewares.js
module.exports = [
// Runs FIRST on request, LAST on response
"strapi::logger",
// Runs SECOND on request, SECOND TO LAST on response
"strapi::errors",
// Runs THIRD on request, THIRD TO LAST on response
{
name: "global::request-timer",
config: {},
},
// Runs FOURTH on request, FIRST on response (innermost)
"strapi::cors",
// ... more middlewares
];
Add your custom middleware in the appropriate position. Middleware that must run early (rate limiting, IP blocking) goes first. Middleware that needs the response (logging, headers) can go in the middle.
Common Mistakes
Not calling
await next(). If you forget to callawait next(), the request hangs and never reaches the controller. Always callawait next()unless you intentionally want to block the request.Placing middleware too late or too early in the stack. Security headers should be added early. Request validation should be after body Parsing. Understand each middleware's dependency.
Creating memory leaks in middleware. Middleware that stores data in closures (like the rate limiter) can leak memory if not cleaned up. Use proper cleanup mechanisms (setInterval for cleanup, WeakMap for references).
Modifying the request object incorrectly. The
ctx.requestandctx.responseobjects have specific APIs. Read Koa documentation to understand how to properly modify requests and responses.Not handling errors in middleware. If middleware throws an unexpected error, it can crash the entire request. Wrap middleware logic in try/catch and use
ctx.throw()for controlled error responses.
Practice Questions
What is the Koa onion model and how does it apply to Strapi middleware? Answer: Middleware runs in a stack pattern. Code before
await next()runs on the request (outer layer first). Code afterawait next()runs on the response (inner layer first). Request goes in through all layers, hits the controller, then response comes back out.How do you create a middleware that only applies to specific routes? Answer: Check
ctx.urlin the middleware and skip processing if it does not match. Or apply the middleware in the specific route configuration using themiddlewaresarray in the route config.What happens if you do not call
await next()in a middleware? Answer: The request stops at that middleware. No subsequent middleware or controller runs. The response is whatever the middleware sends, or nothing (leading to a hanging request).Challenge: Build a complete middleware stack for production: (1) Request ID middleware that assigns a unique ID to every request, (2) Request logger middleware that logs method, URL, IP, and duration, (3) Rate limiter middleware with configurable limits per IP, (4) Security headers middleware that adds all recommended security headers, (5) Maintenance mode middleware that blocks all requests when a flag is set, (6) Response formatter middleware that wraps all responses in a consistent format, (7) Test the complete stack and verify middleware order and functionality.
FAQ
Mini Project
Your task: Build a production-quality middleware stack.
- Create these middleware modules:
request-id: Generates a UUID for every request, adds it to the request and response headersrequest-logger: Logs method, URL, IP, user agent, and response timeip-blocker: Blocks requests from IPs in a configurable blocklistresponse-envelope: Wraps all successful responses in{ data: ..., meta: { ... } }formatcors-custom: Custom CORS middleware with configurable origins
- Register all middleware in
config/middlewares.jsin the correct order. - Test each middleware:
- Request ID: verify every response has X-Request-ID header
- IP blocker: add your IP to the blocklist and verify 403
- Response envelope: verify response format consistency
- Measure the performance impact of the middleware stack.
What's Next
Now that you understand custom middleware, proceed to Webhooks to learn how to configure content-type Webhooks for real-time notifications to external services. After that, explore Strapi with TypeScript.
Related lessons:
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro