Skip to content

Strapi Custom Middleware — Request/Response Middleware, Order, and Config

DodaTech Updated 2026-06-28 9 min read

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 middleware
  • global:: — project middleware (in src/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

  1. Not calling await next(). If you forget to call await next(), the request hangs and never reaches the controller. Always call await next() unless you intentionally want to block the request.

  2. 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.

  3. 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).

  4. Modifying the request object incorrectly. The ctx.request and ctx.response objects have specific APIs. Read Koa documentation to understand how to properly modify requests and responses.

  5. 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

  1. 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 after await next() runs on the response (inner layer first). Request goes in through all layers, hits the controller, then response comes back out.

  2. How do you create a middleware that only applies to specific routes? Answer: Check ctx.url in the middleware and skip processing if it does not match. Or apply the middleware in the specific route configuration using the middlewares array in the route config.

  3. 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).

  4. 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

What is the difference between a middleware and a policy?

Middleware processes requests/responses (logging, headers, rate limiting). Policies control access (authentication, authorization). Middleware runs before policies in the request lifecycle.

Can I use Express middleware in Strapi?

No, Strapi uses Koa which has a different middleware API. Express middleware uses three parameters (req, res, next). Koa uses two (ctx, next). You need to convert or use Koa-compatible middleware.

How do I access the database in a middleware?

Use strapi.db.query() or strapi.entityService in middleware, just like in controllers and services. The strapi object is available as the second parameter of the middleware factory.

Can middleware modify the response after the controller runs?

Yes. Code after await next() runs after the controller has sent its response. You can modify ctx.body, add headers, or transform the response.

How do I test middleware in isolation?

Create a mock Koa context object and call the middleware function directly. Pass a mock next function and verify the context properties and the fact that next was called.

Mini Project

Your task: Build a production-quality middleware stack.

  1. Create these middleware modules:
    • request-id: Generates a UUID for every request, adds it to the request and response headers
    • request-logger: Logs method, URL, IP, user agent, and response time
    • ip-blocker: Blocks requests from IPs in a configurable blocklist
    • response-envelope: Wraps all successful responses in { data: ..., meta: { ... } } format
    • cors-custom: Custom CORS middleware with configurable origins
  2. Register all middleware in config/middlewares.js in the correct order.
  3. 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
  4. 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:

  • API Customization — Controllers, services, and routes
  • Node.js — Understanding Koa middleware
  • REST API — How middleware affects API responses

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro