Skip to content

Middleware

Viames Marino edited this page Mar 26, 2026 · 4 revisions

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 $request
  • callable $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): void;
}

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): void

Executes the middleware stack and then the 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 OPTIONS requests
  • can short-circuit with HTTP 204

ThrottleMiddleware

Uses RateLimiter to restrict request frequency by the best available identity:

  1. sid
  2. bearer token
  3. authenticated user
  4. 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): void
    {
        // Stops the pipeline if the payload is not JSON.
        if (!$request->isJson()) {
            ApiResponse::error('UNSUPPORTED_MEDIA_TYPE');
        }

        // Continues with the next middleware or final action.
        $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): void
    {
        // Requires the Authorization: Bearer header.
        if (!$request->bearerToken()) {
            ApiResponse::error('AUTH_TOKEN_MISSING');
        }

        // Continues only when the token is present.
        $next($request);
    }
}

Require tenant header

class RequireTenantMiddleware implements \Pair\Api\Middleware {

    public function handle(\Pair\Api\Request $request, callable $next): void
    {
        // Reads the custom tenant header.
        $tenant = $request->header('X-Tenant-Id');

        if (!$tenant) {
            // Stops the request with a normalized API error.
            \Pair\Api\ApiResponse::error('BAD_REQUEST', [
                'detail' => 'Missing X-Tenant-Id header',
            ]);
        }

        // Continues the pipeline when the header is present.
        $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): static Appends one middleware to the stack.
  • MiddlewarePipeline::run(Request $request, callable $destination): void Executes 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.

Clone this wiki locally