Skip to content

Middleware

Viames Marino edited this page Oct 19, 2026 · 6 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): 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(...) 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));
}

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

Built-in middleware

CorsMiddleware

Typical responsibilities:

  • emits Access-Control-* headers
  • handles preflight OPTIONS requests
  • can short-circuit by returning an explicit HTTP 204 response

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

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);
    }
}

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());
}

/**
 * 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]);
}

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): mixed Executes the full FIFO pipeline and returns its result.

Common pitfalls

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

Clone this wiki locally