HTTP + WebSockets
Every request is a typed actor message.
Nexus HTTP is not a framework bolted onto an actor system. Handlers are actors. A route resolves to a typed closure, processes one request atomically, and returns a value object — serialized, validated, and traced end-to-end.
<?php
// Declare routes with positional #[Route(method, path)] — no magic strings.
use Monadial\Nexus\Http\Routing\Attribute\Route;
use Monadial\Nexus\Http\Handler\Attribute\FromBody;
#[Route('POST', '/wallets/{id}/deposit')]
final class DepositHandler
{
public function __construct(
private readonly EntityRefFactory $factory,
) {}
public function __invoke(
string $id, // path param resolved by name
#[FromBody] DepositRequest $req, // deserialized from request body
): DepositResponse {
$ref = $this->factory->of($id);
/** @var WalletState $state */
$state = $ref->ask(new Deposit($req->amount), Duration::seconds(2))->await();
return new DepositResponse(balance: $state->balance);
}
}Routing that types itself.
Routes are PHP attributes on handler classes. Path parameters, query strings, and request bodies are injected by name — resolved and cast by the framework, validated before your handler ever runs. No router array, no magic strings. If the types don't match, Psalm catches it at analysis time.
Attribute-driven routes
#[Route(method, path)], #[FromBody],
#[FromActor], #[FromService].
Path parameters are resolved positionally by name. The container resolves handler
dependencies; Psalm verifies the types.
Validation layer
Request DTOs are validated before dispatch. Constraint violations become structured 422 responses — no try/catch scaffolding in handlers.
Return-type serialization
Return a readonly value object. The
framework serializes it to JSON (or your negotiated format) based on
the declared return type. No manual json_encode.
Middleware as a typed pipeline.
Authentication, rate limiting, validation, and tracing are composable pipeline stages — each a named type, each testable in isolation. The runtime short-circuits at the first rejection; your handler never executes with invalid input.
- →
AuthenticationMiddleware— plug in any Bearer JWT, API key, or custom resolver - → Any PSR-15 middleware drops in without modification — CORS, rate limiting, tracing
- →
ExceptionHandlerMiddleware— converts uncaught exceptions to RFC 9457 Problem Details - →
RouterMiddleware— final pipeline stage dispatches to the matched handler
<?php
// Middleware pipeline: stack PSR-15 middleware, then compile.
// Each layer is a named type — composable, independently testable.
use Monadial\Nexus\Http\Dsl\HttpApp;
$compiled = HttpApp::create($system)
->middleware(new AuthenticationMiddleware($jwtVerifier))
->post('/wallets/{id}/deposit', DepositHandler::class)
->compile();
// Hand $compiled to SwooleHttpServerAdapter or any PSR-15-compatible server.<?php
// Inject live actor state directly into a handler parameter.
// #[FromActor] resolves the named ref and awaits the ask — zero boilerplate.
use Monadial\Nexus\Http\Routing\Attribute\Route;
use Monadial\Nexus\Http\Handler\Attribute\FromActor;
#[Route('GET', '/wallets/{id}')]
final class WalletStateHandler
{
public function __invoke(
string $id,
#[FromActor('wallets')] WalletState $state, // actor registered as 'wallets'
): WalletState {
return $state; // already resolved; serialized to JSON by the framework
}
}Ask an actor from inside a handler.
#[FromActor] bridges HTTP and the
actor graph. The framework resolves the actor reference from the route parameters,
issues a typed ask(), awaits the response within
your declared timeout, and injects the result as a handler parameter.
The actor runs concurrently with other requests. Backpressure, mailbox bounds, and supervision are the same as any other actor — the HTTP surface is just another message source.
Handler + actor integration docs →WebSockets: persistent connections, actor semantics.
Each WebSocket connection spawns an actor. Your handler declares a
Behavior — the same model as every other actor
in the system. Pub/sub channels are actor-backed. A dropped connection sends
PostStop; the actor cleans up and terminates.
Under Swoole, thousands of connections run as coroutines. No threads, no polling loop, no external message broker required for local fan-out.
WebSocket handler docs →<?php
// WebSocket handler — extend WebSocketHandler, inject context via #[FromContext].
use Monadial\Nexus\Http\Ws\WebSocket\WebSocketHandler;
use Monadial\Nexus\Http\Ws\WebSocket\WebSocketFrame;
use Monadial\Nexus\Http\Ws\WebSocket\WebSocketContext;
use Monadial\Nexus\Http\Ws\WebSocket\Attribute\FromContext;
final class PriceStreamHandler extends WebSocketHandler
{
public function __construct(
#[FromContext] private readonly WebSocketContext $ctx,
) {}
public function onMessage(WebSocketFrame $frame): void
{
// Echo the frame back to the client.
$this->ctx->send($frame->data);
}
public function onClose(int $code): void
{
// Cleanup on disconnect.
}
}
// Register with WsApplication:
// $app->ws('/prices', PriceStreamHandler::class);Error handling without boilerplate.
Uncaught exceptions in handlers are caught by ExceptionHandlerMiddleware
and converted to RFC 9457 Problem Details responses. Your handlers never need a
try/catch for infrastructure errors — only for
domain logic you intend to surface to callers.
Domain exceptions map to HTTP status codes via a one-line declaration.
AskTimeoutException always produces a
504 Gateway Timeout — the actor took too long and the framework tells
the caller exactly why. No silent 500s.
<?php
// Uncaught handler exceptions produce structured RFC 9457 Problem Details.
// AskTimeoutException automatically becomes HTTP 504.
// Map domain exceptions to HTTP status codes with onException().
use Monadial\Nexus\Http\Dsl\HttpApp;
use Monadial\Nexus\Http\Response\JsonResponse;
$compiled = HttpApp::create($system)
->middleware(new AuthenticationMiddleware($jwtVerifier))
->post('/wallets/{id}/deposit', DepositHandler::class)
->onException(InsufficientFundsException::class,
static fn (InsufficientFundsException $e, ServerRequestInterface $r): ResponseInterface =>
JsonResponse::ok(['error' => $e->getMessage()])->withStatus(422)
)
->onException(AccountFrozenException::class,
static fn (AccountFrozenException $e, ServerRequestInterface $r): ResponseInterface =>
JsonResponse::ok(['error' => 'Account frozen'])->withStatus(403)
)
->compile();<?php
// Nexus HTTP is a first-class PSR-15 application.
// Drop it into any PSR-15-compatible server or use the bundled Swoole adapter.
use Monadial\Nexus\Http\Dsl\HttpApp;
use Monadial\Nexus\Http\Server\Swoole\Server\SwooleHttpServerAdapter;
use Swoole\Http\Server;
$compiled = HttpApp::create($system)
->middleware(new AuthenticationMiddleware($jwtVerifier)) // any PSR-15 middleware
->get('/orders/{id}', OrderController::class)
->compile();
// Swoole adapter — long-lived process, coroutine-per-request
$server = new Server('0.0.0.0', 8080);
(new SwooleHttpServerAdapter($server))->serve($compiled);
// Or hand the PSR-15 compiled app to any compatible server:
// $handler->handle($request); // works with any PSR-15 dispatcherFirst-class PSR-15 application.
Nexus HTTP implements the PSR-15 RequestHandlerInterface.
You can pass it to any PSR-15-compatible server, embed it in a larger middleware
stack, or run it standalone with the bundled Swoole adapter.
The Swoole adapter wraps each request in a coroutine. Thousands of concurrent requests share a tiny number of worker threads — no PHP-FPM process pool needed, no cold-start cost per request.
Standard PSR-15 middleware — CORS, request IDs, structured logging — drops in without modification. If you already have PSR-15 middleware from other packages, it works out of the box.
Ask timeouts under load.
Every HTTP handler that reads actor state uses ask()
internally — and every ask() has a deadline. Under load,
when an actor's mailbox is full and it cannot process your request within the
declared timeout, the framework returns a 504 rather than letting
the connection hang indefinitely.
You set the timeout at the route level or at the
#[FromActor] parameter level. The framework
enforces it. No manual Promise::timeout() wiring.
Backpressure from the actor's bounded mailbox propagates cleanly to HTTP callers as 503 or 504 responses — before your database or downstream services are saturated.
Error handling →<?php
// Per-route ask timeouts prevent slow actors from stalling the HTTP layer.
// AskTimeoutException is caught by the framework and turned into a 504.
use Monadial\Nexus\Http\Routing\Attribute\Route;
use Monadial\Nexus\Http\Handler\Attribute\FromActor;
#[Route('GET', '/orders/{id}')]
final class OrderStatusHandler
{
public function __invoke(
string $id, // path param resolved by name
#[FromActor('orders')] OrderState $state, // ask with actor's default timeout
): OrderState {
return $state; // resolved via ask(); 504 if the actor doesn't respond in time
}
}Ready for production on day one.
Nexus HTTP runs on Swoole — the same runtime as the rest of your actor system. There is no impedance mismatch between your HTTP layer and your worker pool. The same supervision tree covers both. One process. One deployment. One place to look when something goes wrong.
Graceful shutdown
In-flight requests drain before workers stop. No forced terminations.
Structured error responses
Uncaught exceptions produce RFC 9457 Problem Details — never a stack trace in production.
Configurable timeouts
Per-route and per-ask timeouts. AskTimeoutException becomes a 504 automatically.
PSR-3 logging
Request IDs propagated to actor context. Every log line is traceable back to its HTTP origin.
What Nexus HTTP is not.
Nexus HTTP is purpose-built for actor-integrated services. It is not a general-purpose MVC framework. If your application is a standard CRUD service with no concurrency requirements, Symfony or Laravel will be faster to build and easier to staff. Nexus HTTP earns its complexity when your handlers need to talk to long-lived actors — for state reads, command dispatch, or real-time subscriptions.
Similarly, Nexus HTTP does not replace a reverse proxy. Run Nginx or Caddy in front for TLS termination, static assets, and connection pooling at the edge. Nexus HTTP handles the application tier — fast, typed, and actor-aware.
Security at the request boundary.
The ask() timeout on every handler call is a
security control as much as a reliability one. A flood of slow or malicious
requests cannot saturate the actor indefinitely — each request has a deadline,
and AskTimeoutException becomes a 504 before
upstream connections are exhausted. Pair this with a bounded mailbox on the
actor to cap the queue depth: backpressure propagates cleanly to HTTP callers
as 503 or 504 responses before your database or downstream services are
saturated.
PSR-15 middleware handles authentication, CSRF validation, and input sanitisation
before the request becomes an actor message. Keep the validation layer
outside the actor topology — the actor should receive only well-formed, already-authenticated
commands. Each request handler should hold its own ActorRef;
do not share controller instances across requests that need different actor scopes,
as this risks leaking context between callers.
Build your first typed HTTP handler.
The quick-start covers routing, validation, and actor integration in under ten minutes.
composer require nexus-actors/nexus