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 the actor reference registered with HttpApp — then ask it.
// #[FromActor] resolves the ref; the ask stays explicit in your handler.
use Monadial\Nexus\Core\Actor\ActorRef;
use Monadial\Nexus\Http\Routing\Attribute\Route;
use Monadial\Nexus\Http\Handler\Attribute\FromActor;
use Monadial\Nexus\Runtime\Duration;
#[Route('GET', '/wallets/{id}')]
final readonly class WalletStateHandler
{
public function __invoke(
string $id,
#[FromActor('wallets')] ActorRef $wallets, // actor registered as 'wallets'
): WalletState {
/** @var WalletState — serialized to JSON by the framework */
return $wallets->ask(new GetWalletState($id), Duration::seconds(2))->await();
}
}Ask an actor from inside a handler.
#[FromActor] bridges HTTP and the actor graph. The framework injects the ActorRefregistered under that name — a shared singleton, or an actor spawned just for this request. Your handler issues a typed ask()with an explicit deadline and returns the reply.
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 aBehavior — the same model as every other actor in the system. Pub/sub channels are actor-backed. A dropped connection sendsPostStop; 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 ExceptionHandlerMiddlewareand converted to RFC 9457 Problem Details responses. Your handlers never need atry/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 a504 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.
The timeout is the Duration you pass to eachask() — the deadline lives next to the call that needs it. AskTimeoutException is mapped to 504 by the framework. 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
// Every ask() carries a deadline. If the actor can't reply in time,
// AskTimeoutException surfaces — the framework maps it to HTTP 504.
use Monadial\Nexus\Core\Actor\ActorRef;
use Monadial\Nexus\Http\Routing\Attribute\Route;
use Monadial\Nexus\Http\Handler\Attribute\FromActor;
use Monadial\Nexus\Runtime\Duration;
#[Route('GET', '/orders/{id}')]
final readonly class OrderStatusHandler
{
public function __invoke(
string $id, // path param resolved by name
#[FromActor('orders')] ActorRef $orders,
): OrderState {
// The deadline lives where the call happens — no hidden config.
return $orders->ask(new GetOrderStatus($id), Duration::millis(500))->await();
}
}One runtime for HTTP and actors.
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 sanitisationbefore 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