Middleware - viames/pair GitHub Wiki
Pair framework: Middleware (API)
Pair API middleware is based on the Pair\Api\Middleware interface plus Pair\Api\MiddlewarePipeline.
Middleware is the reusable request-guard layer of the Pair API stack.
Use it for cross-cutting concerns such as:
- CORS
- throttling
- bearer-token checks
- tenant or workspace headers
- feature flags or maintenance windows
Core contract
Every middleware receives:
Request $requestcallable $next
and can either:
- continue the pipeline with
$next($request) - stop the request immediately by returning an API response
Interface
interface Middleware {
public function handle(Request $request, callable $next): mixed;
}
This single method is the main method of every middleware class.
How the pipeline works
MiddlewarePipeline runs middleware in FIFO order:
- first added = first executed
- last added = closest to the final action
ApiController exposes this through:
$this->middleware(...)$this->runMiddleware(function () { ... })
Example:
protected function _init(): void
{
parent::_init();
// The default throttle is already mounted by ApiController.
$this->middleware(new \Pair\Api\CorsMiddleware());
$this->middleware(new RequireBearerMiddleware());
}
public function ordersAction(): void
{
$this->runMiddleware(function () {
// Runs only after all middleware pass.
\Pair\Api\ApiResponse::respond(['ok' => true]);
});
}
Important: ApiController currently mounts the default throttle inside parent::_init(), so middleware added later in _init() runs after that throttle.
If you need a different order, override registerDefaultMiddleware().
protected function registerDefaultMiddleware(): void
{
// Puts CORS before the throttle for this controller family.
$this->middleware(new \Pair\Api\CorsMiddleware());
$this->middleware(new \Pair\Api\ThrottleMiddleware(60, 60));
}
Main integration points
middleware(Middleware $middleware): void
Registers one middleware in the controller pipeline.
runMiddleware(callable $destination): mixed
Executes the middleware stack and returns the first explicit value produced by the middleware chain or final action callback.
These are the two API-controller methods you will use most often when working with middleware.
Built-in middleware
CorsMiddleware
Typical responsibilities:
- emits
Access-Control-*headers - handles preflight
OPTIONSrequests - can short-circuit by returning an explicit HTTP
204response
ThrottleMiddleware
Uses RateLimiter to restrict request frequency by the best available identity:
sid- bearer token
- authenticated user
- client IP
Example:
// Allows up to 120 requests every 60 seconds.
new \Pair\Api\ThrottleMiddleware(120, 60);
When the limit is exceeded, Pair returns TOO_MANY_REQUESTS with HTTP 429, Retry-After, and X-RateLimit-* headers.
Custom middleware examples
Require JSON requests
use Pair\Api\Middleware;
use Pair\Api\Request;
use Pair\Api\ApiResponse;
class RequireJsonMiddleware implements Middleware {
public function handle(Request $request, callable $next): mixed
{
// Returns an explicit error response if the payload is not JSON.
if (!$request->isJson()) {
return ApiResponse::errorResponse('UNSUPPORTED_MEDIA_TYPE');
}
// Continues with the next middleware or final action.
return $next($request);
}
}
Require Bearer token
use Pair\Api\Middleware;
use Pair\Api\Request;
use Pair\Api\ApiResponse;
class RequireBearerMiddleware implements Middleware {
public function handle(Request $request, callable $next): mixed
{
// Requires the Authorization: Bearer header.
if (!$request->bearerToken()) {
return ApiResponse::errorResponse('AUTH_TOKEN_MISSING');
}
// Continues only when the token is present.
return $next($request);
}
}
Require tenant header
class RequireTenantMiddleware implements \Pair\Api\Middleware {
public function handle(\Pair\Api\Request $request, callable $next): mixed
{
// Reads the custom tenant header.
$tenant = $request->header('X-Tenant-Id');
if (!$tenant) {
// Returns the normalized API error without sending output directly.
return \Pair\Api\ApiResponse::errorResponse('BAD_REQUEST', [
'detail' => 'Missing X-Tenant-Id header',
]);
}
// Continues the pipeline when the header is present.
return $next($request);
}
}
Practical pattern: protected API controller
protected function _init(): void
{
parent::_init();
// Adds CORS after the default throttle.
$this->middleware(new \Pair\Api\CorsMiddleware());
// Requires a bearer token for every action in this controller.
$this->middleware(new RequireBearerMiddleware());
}
public function meAction(): void
{
$this->runMiddleware(function () {
// The destination runs only after all middleware pass.
\Pair\Api\ApiResponse::respond(['ok' => true]);
});
}
Secondary building blocks worth knowing
Middleware itself has only one public contract method, but the surrounding pipeline is equally important:
MiddlewarePipeline::add(Middleware $middleware): staticAppends one middleware to the stack.MiddlewarePipeline::run(Request $request, callable $destination): voidExecutes the full FIFO pipeline.
Common pitfalls
- Calling
$next()more than once inside the same middleware. - Forgetting to call
$next($request)when the middleware should allow the request through. - Adding a second throttle middleware without accounting for the default one already added by
ApiController. - Assuming CORS runs before the default throttle when you only append middleware after
parent::_init(). - Putting CORS after auth/throttle when preflight requests should short-circuit first.
See also: MiddlewarePipeline, RateLimiter, ThrottleMiddleware, Request, API.