-
Notifications
You must be signed in to change notification settings - Fork 2
API
Pair includes a native API layer designed for JSON endpoints with authentication helpers, middleware pipeline, automatic CRUD resources, throttling, CORS, idempotency, and OpenAPI generation helpers.
Pair\Api\ApiControllerPair\Api\CrudControllerPair\Api\RequestPair\Api\ApiResponse-
Pair\Api\Middleware+Pair\Api\MiddlewarePipeline Pair\Api\CorsMiddleware-
Pair\Api\ThrottleMiddleware+Pair\Api\RateLimiter Pair\Api\IdempotencyPair\Api\ResourcePair\Api\QueryFilterPair\Api\PasskeyControllerPair\Services\PasskeyAuthPair\Models\UserPasskey
<?php
namespace App\Modules\Api;
use Pair\Api\ApiController as BaseApiController;
use Pair\Api\ApiResponse;
class ApiController extends BaseApiController {
protected function _init(): void
{
parent::_init();
}
public function healthAction(): void
{
ApiResponse::respond(['ok' => true]);
}
}CrudController can register ActiveRecord models and expose REST-style endpoints automatically.
<?php
namespace App\Modules\Api;
use Pair\Api\CrudController as BaseCrudController;
use App\Models\Faq;
class ApiController extends BaseCrudController {
protected function _init(): void
{
parent::_init();
$this->crud('faqs', Faq::class);
}
}Generated endpoints (for faqs):
GET /api/faqsGET /api/faqs/{id}POST /api/faqs-
PUT /api/faqs/{id}(alsoPATCH) DELETE /api/faqs/{id}
Request provides method/content-type/body/query access:
$method = $this->request->method();
$isJson = $this->request->isJson();
$body = $this->request->json();
$status = $this->request->query('status');ApiResponse provides consistent JSON output:
ApiResponse::respond(['saved' => true], 201);
ApiResponse::error('BAD_REQUEST', ['detail' => 'Invalid payload']);
ApiResponse::paginated($rows, $page, $perPage, $total);ApiController includes a middleware pipeline:
$this->middleware(new \Pair\Api\CorsMiddleware());
$this->middleware(new \Pair\Api\ThrottleMiddleware(60, 60));
$this->runMiddleware(function () {
// action code
});Production order suggestion:
- CORS middleware first (preflight short-circuit).
- Throttle middleware second.
- Authentication/authorization middleware after transport checks.
- Endpoint action last.
Inside ApiController:
-
requireAuth()requires an authenticated user. -
requireBearer()requires a bearer token. -
getUser()returns current user ornull. -
requireJsonPost()validates method + JSON content-type + body.
Useful for replayed requests (mobile retries, offline queue replays, flaky networks):
use Pair\Api\Idempotency;
use Pair\Api\ApiResponse;
Idempotency::respondIfDuplicate($this->request, 'orders:create');
$result = ['orderId' => 123, 'saved' => true];
Idempotency::storeResponse($this->request, 'orders:create', $result, 201);
ApiResponse::respond($result, 201);Pair provides PasskeyController for ready-to-use endpoints:
POST /api/passkey/login/optionsPOST /api/passkey/login/verify-
POST /api/passkey/register/options(requires authenticated session) -
POST /api/passkey/register/verify(requires authenticated session) -
GET /api/passkey/list(requires authenticated session) -
DELETE /api/passkey/revoke/{id}(requires authenticated session)
Minimal setup:
class ApiController extends \Pair\Api\PasskeyController {}Frontend integration is usually done with PairPasskey.js.
public function createOrderAction(): void
{
$this->runMiddleware(function () {
$this->requireAuth();
$this->requireJsonPost();
$data = $this->request->validate([
'customerId' => 'required|int',
'amount' => 'required|numeric|min:0.01',
'currency' => 'required|string|max:3',
]);
$order = new \App\Orm\Order();
$order->customerId = (int)$data['customerId'];
$order->amount = (float)$data['amount'];
$order->currency = strtoupper((string)$data['currency']);
if (!$order->store()) {
ApiResponse::error('INVALID_OBJECT_DATA', ['errors' => $order->getErrors()]);
}
ApiResponse::respond(['id' => $order->getId()], 201);
});
}public function createPaymentAction(): void
{
$this->requireAuth();
$this->requireJsonPost();
Idempotency::respondIfDuplicate($this->request, 'payments:create');
try {
$payload = $this->request->validate([
'orderId' => 'required|int',
'amount' => 'required|numeric|min:0.01',
]);
$result = ['paymentId' => 9911, 'orderId' => (int)$payload['orderId']];
Idempotency::storeResponse($this->request, 'payments:create', $result, 201);
ApiResponse::respond($result, 201);
} catch (\Throwable $e) {
Idempotency::clearProcessing($this->request, 'payments:create');
throw $e;
}
}protected function _init(): void
{
parent::_init();
$this->crud('users', \App\Orm\User::class);
}
public function meAction(): void
{
$user = $this->requireAuth();
ApiResponse::respond($user->toArray());
}- Overriding
ApiController::_init()and forgettingparent::_init(). - Returning non-JSON output (echo/print) in API actions.
- Running
lockForUpdate()logic without transaction control. - Using idempotency key without
clearProcessing()in failure paths.
Pair provides generators under:
Pair\Api\OpenApi\SpecGeneratorPair\Api\OpenApi\SchemaGenerator
Use them to build machine-readable API docs from your resource/controller metadata.
- Application
- Router
- ActiveRecord
- Aircall API integration
- AircallClient
- ApiController
- ApiExposable
- CrudController
- PasskeyController
- PasskeyAuth
- UserPasskey
- Request
- ApiResponse
- Idempotency
- QueryFilter
- Middleware
- MiddlewarePipeline
- CorsMiddleware
- ThrottleMiddleware
- Resource
- RateLimiter
- Query
- Database
- SpecGenerator
- SchemaGenerator
- PairException
- AppException
- ApiException
- AjaxException
- CriticalException
- ErrorCodes
- PairPasskey.js
- PairPush.js
- PWA