Skip to main content
A middleware in Apivalk is any class implementing MiddlewareInterface. It sits in the MiddlewareStack between the router and the controller, gets a typed request + the resolved controller + a $next callable, and must return an AbstractApivalkResponse.

The contract

Three things to know:
  1. Call $next($request) exactly once to continue the pipeline. Return its value (or a wrapper) to the caller.
  2. Short-circuit by returning a response without calling $next. That’s how auth, rate limit, validation failures, feature flags, etc. bail out.
  3. The stack is onion-shaped. Middlewares added last wrap the controller innermost. See the order rules below.

Example: log request + response

Register it:

Example: short-circuit feature flag

Returning without calling $next() stops the pipeline. Everything outside this middleware still runs on the response — notably, the MiddlewareStack still appends Content-Language and (if applicable) X-RateLimit-* headers after all middlewares return.

Example: mutate the request

Middlewares are the right place to stamp correlation IDs onto outbound responses, because they’re the only layer that sees every response uniformly.

Order rules

MiddlewareStack::handle() reverses the list internally, so the last middleware you add() is the innermost layer (closest to the controller). Practical consequences:
  • Authentication → Security. Add AuthenticationMiddleware before SecurityMiddleware, so the identity is populated before SecurityMiddleware reads it.
  • Rate limit early. Add RateLimitMiddleware early so 429s don’t do expensive work. You still want it after any middleware that sets the identity if your RateLimitInterface::getKey() reads identity.
  • Validate last. RequestValidationMiddleware touches the actual request body and should be closest to the controller.
  • Cross-cutting observability (log, request id, metrics). Add first — they wrap everything, see both entry and exit of the whole stack.
Recommended default order:

Middleware vs controller logic

Use a middleware when:
  • The behaviour applies to many routes — auth, rate limit, logging, request id.
  • You want to short-circuit before the controller builds state.
  • You need to see both request and response uniformly.
Use a controller (or a service called from one) when:
  • The logic is route-specific — only CreateAnimalController validates against inventory.
  • The logic requires typed access to declared properties — body / path / query — which the middleware only sees as generic bags.

Register via the container (optional)

For a middleware with dependencies (logger, cache, feature-flag service, database), define it in your PSR-11 container and pull it out at bootstrap:
See Configure Apivalk → extending your configuration.