pushinbr/pam-http
pushinbr/pam-http is PAM’s optional HTTP application layer. It combines
Express-like route ergonomics with Laravel-style application boundaries while
keeping the persistent PAM runtime explicit and safe.
Start here
Section titled “Start here”Every PAM product runs on the PAM runtime. Install the runtime once, verify the machine, create the project, and then use PAM’s Composer passthrough to add the HTTP package:
curl -fsSL https://github.com/push-in/pam/releases/latest/download/install.sh | shpam doctorpam init my-api --template httpcd my-apipam composer require pushinbr/pam-httppam devRegister routes
Section titled “Register routes”<?php
declare(strict_types=1);
use Pam\App;
$app = new App();
$app->get('/users/{id}', static fn ($request, $response) => $response->json([ 'id' => $request->route('id'), ]));
$app->post('/users', static function ($request, $response) { $payload = $request->json();
return $response->json([ 'name' => $payload['name'] ?? null, ], 201);});
$app->listen(3000);Controllers and method mapping
Section titled “Controllers and method mapping”Routes accept closures, invokable controllers, or an explicit controller and method pair:
$app->post('/orders', CreateOrderController::class);$app->post('/login', [LoginController::class, 'onLogin']);Class-and-method handlers are checked when the route is registered. PAM resolves the controller through its container, injects constructor services, and injects request, response, container services, and named route parameters into the action method.
final readonly class LoginController{ public function __construct(private LoginService $login) {}
public function onLogin(LoginRequest $request): AuthResource { return new AuthResource($this->login->handle($request->validated())); }}Fluent routes and groups
Section titled “Fluent routes and groups”use Pam\Api\RouteConstraint;use Pam\Api\RouteRegistrar;
$app->prefix('/api/v1') ->middleware(Authenticate::class) ->group(function (RouteRegistrar $routes): void { $routes->apiResource('/users', UserController::class);
$routes->get('/users/{id}', [UserController::class, 'show']) ->where('id', RouteConstraint::Integer) ->name('users.show'); });Built-in constraints cover integers, UUIDs, ULIDs, slugs, alphabetic values and
alphanumeric values. where() also accepts an explicit regular expression.
Container and request scope
Section titled “Container and request scope”$container = $app->container();$container->bind(UserRepository::class, DatabaseUserRepository::class);$container->singleton(Cache::class, RedisCache::class);$container->scoped(CurrentUser::class);Transient bindings create a value for each resolution. Singletons live for the
worker. Scoped values live only for the current request and are discarded in a
finally boundary, including exceptional responses. Never place request or
tenant state in a singleton.
Form Requests
Section titled “Form Requests”use Pam\Api\Validation\FormRequest;use Pam\Api\Validation\Rule;
final class LoginRequest extends FormRequest{ public function rules(): array { return [ 'email' => ['required', 'string', 'email'], 'type' => ['required', Rule::enum(UserType::class)], ]; }}
enum UserType: int{ case Regular = 1; case Administrator = 2;}Validation and authorization stay outside controllers. Validation failures use Problem Details and stable integer error codes. Status/type/state/kind/category values should be sequential integer-backed enums.
Validated values can be hydrated into a readonly DTO, including backed enum constructor arguments:
$data = $request->dto(CreateUserData::class);JSON Resources
Section titled “JSON Resources”Handlers may return JsonResource or ResourceCollection. PAM converts them
into consistent data and optional meta envelopes:
final readonly class UserResource extends JsonResource{ public function toArray(Request $request): array { return [ 'id' => $this->resource->id, 'name' => $this->resource->name, 'type' => $this->resource->type->value, ]; }}Pass a valid HTTP status as the second constructor argument when the action creates a resource:
public function store(StoreUserRequest $request): UserResource{ return new UserResource($this->users->create($request->data()), 201);}Codes outside 100..599 are rejected during construction. Collections retain
the normal 200 status.
Convenience methods exist for GET, POST, PUT, PATCH, DELETE, HEAD
and OPTIONS. Use route() for another valid HTTP method. When no explicit
OPTIONS route exists, PAM returns 204 with a deterministic Allow header.
Every GET route advertises and matches HEAD; explicit handlers take
precedence. The final HEAD body is suppressed after middleware and error
handling while status and headers are preserved.
Route paths must be absolute and cannot include a query string. A parameter occupies an entire segment:
$app->get('/teams/{team}/members/{member}', $handler);Parameter names must be unique and match [A-Za-z_][A-Za-z0-9_]*. Static paths are normalized for a trailing slash. HEAD falls back to a matching GET route.
Route configuration has explicit resource limits: 10,000 routes by default (configurable up to 100,000), 2 KiB paths, 128 segments, 32 parameters and 512-byte custom constraints. Compiled patterns carry PCRE match/depth budgets; oversized request paths are rejected before regex matching.
Routing outcomes
Section titled “Routing outcomes”- A matching route receives decoded route parameters.
- A path with no route returns Problem Details
404. - A known path with the wrong method returns Problem Details
405, code9, and anAllowheader. - Registering the same method and path twice throws during application configuration.
Middleware
Section titled “Middleware”Middleware can implement PAM’s MiddlewareInterface or be a callable:
use Pam\Contracts\Http\RequestHandlerInterface;use Pam\Http\Request;use Pam\Http\Response;
$app->middleware( static function ( Request $request, Response $response, RequestHandlerInterface $next, ): Response { $response = $next->handle($request, $response);
return $response->header('x-runtime', 'pam'); },);The pipeline is compiled once, in registration order, when the application first handles work. Every middleware layer must return Pam\Http\Response.
Error handling
Section titled “Error handling”Routing, operational and unexpected failures share one Problem Details
contract with application/problem+json. The default boundary logs the full
exception through PAM telemetry and returns only:
{"type":"https://pam.dev/problems/8","title":"Internal Server Error","status":500,"code":8}It does not expose the exception message to the client. Domain details attached
to HttpException may add fields but cannot override type, title, status,
or code.
Customize the boundary before the application starts:
$app->onError( static fn (Throwable $error, $response) => $response->json(['error' => 'Request failed'], 500),);The custom handler must return a PAM response.
Providers and discovery
Section titled “Providers and discovery”Service providers implement register() and boot(). Registration happens while the application is mutable; providers boot when the route and middleware pipeline freezes.
Console tools can call the idempotent $app->boot() method to boot providers
and freeze configuration without dispatching a synthetic HTTP request. The
generated migration command uses this boundary.
Composer package discovery can register providers from installed package metadata. Pass discoverPackages: false to new App() when configuration must be fully explicit.
Eloquent ORM
Section titled “Eloquent ORM”Eloquent is the official PAM API ORM. Its connection resolver creates one Illuminate manager per request Fiber and automatically rolls back unfinished transactions and disconnects connections at the request boundary.
use Pam\Api\Database\DatabaseConfig;use Pam\Api\Database\EloquentServiceProvider;
$app->provider(new EloquentServiceProvider(DatabaseConfig::fromEnvironment()));DB_CONNECTION accepts pgsql, mysql, sqlite, or sqlsrv; PostgreSQL is
the production default. Models, events, schema builders and anonymous-class
Laravel migrations work normally. MigrationManager supports migrate,
rollback, named connections and dry-run execution.
Run schema changes as an explicit deployment step. The starter exposes
pam composer migrate; it never races migrations across HTTP workers during
application startup.
The generated pam-skeleton is the executable reference
architecture: thin named controller methods, Form Request and DTO, service,
repository abstraction, Eloquent implementation, migration, Resource, SQLite
tests, and integer-backed domain status.
Use QueryBudgetMiddleware to fail tests or development requests that exceed
query count, accumulated SQL time or duplicate-query limits. This turns N+1
detection into an executable route contract instead of a manual review.
For tenant-owned models, register TenantModelGuard during boot. It applies a
global scope, assigns tenant_id on creation, rejects cross-tenant assignment
and fails closed when no scoped TenantContext exists.
Typed configuration
Section titled “Typed configuration”use Pam\Api\Config\ConfigDefinition;use Pam\Api\Config\Configuration;use Pam\Api\Config\ConfigType;
$config = Configuration::fromEnvironment([ new ConfigDefinition('app.port', 'PAM_PORT', ConfigType::Integer), new ConfigDefinition('auth.secret', 'AUTH_SECRET', sensitive: true),]);Configuration is validated before the application serves requests. Diagnostics
must use redacted(), which masks sensitive values. Error messages name the
missing variable and expected type without echoing the supplied value.
Signed bearer authentication
Section titled “Signed bearer authentication”HmacTokenCodec issues bounded HS256 access tokens with issuer, audience,
iat, nbf, exp, jti and abilities. BearerTokenAuthenticator integrates
the codec with AuthenticateMiddleware.
The active signing key has a bounded kid; up to four retiring keys may remain
in the verification keyring for zero-downtime rotation. Unknown key IDs fail
closed. A TokenRevocationStore can revoke a jti; the included memory store
is bounded and intended only for development and tests. Production clusters
must implement the contract with atomic shared state.
Access tokens cannot live longer than 24 hours. Refresh-token rotation and OAuth/OIDC authorization-server behavior still belong to an identity provider or a separately audited application flow.
Request lifecycle and PAM Lens
Section titled “Request lifecycle and PAM Lens”$app->observe($observer) registers a RequestLifecycleObserver. Observers
start in registration order and finish in reverse order before request-scoped
state is destroyed. They receive the handled failure when present; an observer
cleanup failure is logged and cannot stop the remaining cleanup chain.
MemoryProfiler is the bounded first PAM Lens collector. It is disabled by
default and must be enabled with ProfilerMode::Development or
ProfilerMode::Testing. Profiles retain only method, path, status, duration,
memory delta and failure class. Headers, bearer tokens and request/response
payloads are never stored.
PAM API generators
Section titled “PAM API generators”Inside a PAM API project:
pam make:model Userpam make:controller UserControllerpam make:request StoreUserRequestpam make:resource UserResourcepam make:service CreateUserServicepam make:repository UserRepositorypam make:migration CreateUsersTableNames must be bounded PascalCase identifiers. Generators create the expected
application directories and refuse to overwrite existing files. Controllers
use named actions and are ready for handlers such as
[UserController::class, 'index'].
Job workers
Section titled “Job workers”JobQueue defines an at-least-once lifecycle for durable adapters: bounded
dispatch delay, reservation leases, acknowledgement, delayed release and
dead-letter transitions. JobWorker runs the same contract against the bounded
memory queue in development and tests. Expired leases may be reclaimed, failed
attempts retry up to the envelope limit, and exhausted jobs move to the
dead-letter set. Diagnostics retain the failure class, never its message.
Durable adapters encode SerializableJob values with JobCodec. Its JSON
envelope is versioned, limited to 64 KiB and depth 16, and resolves job names
through an explicit registry. It does not accept PHP serialized objects or
client-controlled class names.
Configuration freeze
Section titled “Configuration freeze”Routes, middleware, providers, error handlers, and PSR handlers cannot be changed after the application begins handling requests. PAM throws rather than silently mutating a live long-lived pipeline.
Public API compatibility
Section titled “Public API compatibility”composer verify includes composer api:compat, which compares PAM API’s
public classes, interfaces, enums, methods, parameters, properties and
constants with the committed api-surface.json. Unreviewed additions fail as
baseline drift. Removals, signature changes, new interface/abstract methods,
newly final or abstract contracts and enum changes fail as incompatibilities;
all findings use sequential integer codes.
Run bin/api-compat update after approving compatible additions. Updating the
baseline for breaking changes is allowed only on an intentional major-version
branch after its upgrade guide and changelog describe every break. Normal CI
never regenerates the baseline.
Public API reference
Section titled “Public API reference”The generated 118-symbol reference lists exact constructors, methods, parameters, return types, properties, constants and integer enum cases. It is generated from the same reviewed surface baseline used by the SemVer gate, and CI rejects stale output.
| Type | Public surface |
|---|---|
App |
Route verbs, prefix(), group(), container(), middleware, providers, errors, listener and request handling |
Router |
add(method, path, handler), match(method, path), routes() |
RouteRegistrar |
Prefix/group composition and REST resource registration |
PendingRoute |
Route names, constraints and route middleware |
Container |
Transient, singleton and request-scoped dependency resolution |
FormRequest |
Dedicated authorization and input validation |
JsonResource |
Domain response transformation and data envelopes |
Pipeline |
Construct with middleware and a destination; execute with handle() |
CallableRequestHandler |
Adapts a callable to RequestHandlerInterface |
Route |
Method, path, compiled handler, matching metadata and route middleware |
RoutingResult |
Readonly type, matched route, decoded parameters and allowed methods |
PackageDiscovery |
providers(projectRoot) reads Composer package provider metadata |
EloquentManager |
Fiber-local connections, schemas and transactions |
MigrationManager |
Laravel-compatible migration and rollback execution |
Configuration |
Boot-time typed configuration and secret-safe diagnostics |
HmacTokenCodec |
Signed access tokens, bounded key rotation and verification |
RequestLifecycleObserver |
Failure-aware request start/finish instrumentation |
MemoryProfiler |
Bounded privacy-safe development request profiles |
JobQueue |
Lease, acknowledgement, retry and dead-letter adapter contract |
JobWorker |
Bounded one-job execution with at-least-once semantics |
JobCodec |
Allowlisted and bounded JSON serialization for durable jobs |
RoutingResultType is integer-backed: Found=1,
MethodNotAllowed=2, NotFound=3.
Included middleware
Section titled “Included middleware”use Pam\Api\Middleware\CorsMiddleware;use Pam\Api\Middleware\RateLimitMiddleware;use Pam\Api\Middleware\SecurityHeadersMiddleware;
$app ->middleware(new CorsMiddleware( origins: ['https://app.example.com'], credentials: true, maxAge: 600, )) ->middleware(new RateLimitMiddleware( requestsPerSecond: 20, burst: 40, maxBuckets: 65_536, idleTtlSeconds: 300, )) ->middleware(new SecurityHeadersMiddleware(hsts: true));CorsMiddleware handles matching origins and terminates valid preflight
requests with 204. Never combine wildcard origins with credentials.
RateLimitMiddleware is a per-worker, in-memory token bucket keyed by
REMOTE_ADDR by default. Pass a RateLimitStore backed by Redis or another
atomic shared system when limits must be global across workers, and pass a
custom key resolver for user, token or tenant limits. Proxy-derived client
addresses must only be accepted behind an explicitly trusted proxy boundary.
Limit, remaining and retry headers are emitted with a Problem Details 429.
SecurityHeadersMiddleware adds content-type, frame, referrer and permissions
policies; HSTS is added only when HTTPS=on.
Authentication, idempotency, and tenants
Section titled “Authentication, idempotency, and tenants”Authentication uses application-provided Authenticator and Principal
contracts. The authenticated principal and resolved tenant exist only in the
current request scope. Ability checks never retain user state in a worker
singleton.
IdempotencyMiddleware replays an identical request and rejects reuse of the
same key with a different payload. Response caching and idempotency both use
replaceable stores; bounded memory stores are included for development and
tests, while multiworker deployments should provide atomic shared adapters.
OpenAPI and clients
Section titled “OpenAPI and clients”$app->post('/users', [UserController::class, 'store']) ->name('users.store') ->summary('Create a user') ->tags(['Users']) ->input(StoreUserRequest::class) ->output(UserResource::class);
$contract = $app->openApi('My API', '1.0.0');$json = $contract->toJson();$typescript = $contract->client(ClientLanguage::TypeScript);Kotlin and Swift clients use the same OpenAPI 3.1 contract. Compatibility checks report removed paths and operations through sequential integer codes.
Performance regression contract
Section titled “Performance regression contract”composer benchmark emits schema 1 JSON for 100 routes plus one constrained
dynamic route. It performs warm-up, measures static and dynamic paths
independently, and reports throughput, p50/p95/p99 latency and peak memory.
composer verify runs the assertion mode with conservative throughput, p99 and
memory ceilings. These gates detect severe regressions; they are not portable
marketing comparisons because host hardware and load topology still matter.
Production and testing contracts
Section titled “Production and testing contracts”The package includes replaceable contracts for transactions, events, jobs,
cache, retry, circuit breakers, deadlines, health checks, tenant resolution and
request observations. SSE remains in PAM’s native response API; WebSocket rooms
and events remain in pushinbr/pam-socket.
TestClient executes the real application pipeline without a network socket
and provides status, header, JSON and JSON-path assertions. Container
diagnostics expose request-scope state for leak checks.