Repository navigation
Middleware
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
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 Middleware {
public function handle(Request $request, callable $next): mixed;
}This single method is the main method of every middleware class.
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(...)for manual dispatch outside the normal request lifecycle
During a normal API request, Application runs the pipeline before invoking the action. Return the response directly from the action; calling runMiddleware() there would execute guards and consume throttle hits twice.
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());
}
/**
* Return orders after the runtime middleware checks.
*/
public function ordersAction(): \Pair\Http\JsonResponse {
return $this->jsonResponse(['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));
}Registers one middleware in the controller pipeline.
Executes the middleware stack and returns the first explicit value produced by the middleware chain or final action callback. Normal API dispatch already calls this method. Manual callers must propagate or send the returned response, including short-circuit errors.
These are the two API-controller methods you will use most often when working with middleware.
Typical responsibilities:
- emits
Access-Control-*headers - handles preflight
OPTIONSrequests - can short-circuit by returning an explicit HTTP
204response
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.
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);
}
}This example checks header presence only. It does not validate the token or authenticate a user; Pair's runtime authentication and requireAuthOrResponse() still own those checks.
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);
}
}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);
}
}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());
}
/**
* Return the current user after authentication and middleware checks.
*/
public function meAction(): \Pair\Http\ResponseInterface {
$user = $this->requireAuthOrResponse();
if ($user instanceof \Pair\Api\ApiErrorResponse) {
return $user;
}
return $this->jsonResponse(['id' => $user->id]);
}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): mixedExecutes the full FIFO pipeline and returns its result.
- Calling
$next()more than once inside the same middleware. - Forgetting to call
$next($request)when the middleware should allow the request through. - Calling
runMiddleware()from an action already dispatched through the runtime pipeline. - 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.
Home · Setup · Configuration
Build with Pair 4
- Generator
- Controllers and responses
- Read models
- ActiveRecord and queries
- Forms and validation
- PairUI.js and HTML fragments
APIs and authentication
- API guide and CRUD
- Middleware and rate limits
- Users, sessions, and ACL
- Passkeys and social login
- Native mobile setup
Integrate and operate
- Integrations
- Runtime extensions and adapter keys
- Cache and cache stores
- Logs and observability
- PWA and push
- Packages and distribution
- Upgrade to v4