Responses
Handlers must return a Psr\Http\Message\ResponseInterface. Nexus ships two convenience factories — Response for status-only and plain bodies, JsonResponse for JSON — plus StreamingResponse for chunked output.
Response
Status-only helpers cover the common cases:
use Monadial\Nexus\Http\Response\Response;
Response::ok(); // 200, empty body
Response::noContent(); // 204
Response::created('/orders/42'); // 201 + Location
Response::badRequest('Invalid SKU'); // 400, body = message
Response::notFound('Order not found'); // 404
Response::gatewayTimeout(); // 504
Response::serviceUnavailable(Duration::seconds(60)); // 503 + Retry-After: 60
Response::internalServerError(); // 500
Each returns a fully-formed PSR-7 response. Chain ->withHeader(...) or ->withBody(...) to customise:
return Response::created('/orders/42')
->withHeader('X-Trace-Id', $traceId);
JsonResponse
use Monadial\Nexus\Http\Response\JsonResponse;
JsonResponse::ok(['items' => $orders]);
JsonResponse::created(['id' => 42], '/orders/42');
The body is JSON-encoded with JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE. The Content-Type: application/json; charset=utf-8 header is set automatically. Override encoding flags:
return JsonResponse::ok($data, JSON_PRETTY_PRINT);
For status codes outside the ok / created helpers, chain ->withStatus():
return JsonResponse::ok(['error' => 'validation', 'fields' => $errors])
->withStatus(422);
StreamingResponse
For long-lived bodies — NDJSON exports, server-sent events, large file downloads — wrap an iterable:
use Monadial\Nexus\Http\Response\StreamingResponse;
return new StreamingResponse(
static function () use ($db) {
foreach ($db->stream('SELECT * FROM events') as $row) {
yield json_encode($row) . "\n";
}
},
headers: ['Content-Type' => 'application/x-ndjson'],
);
The generator runs lazily. Each yielded string becomes a chunk on the wire. Memory stays constant regardless of total response size.
Server-Sent Events
return new StreamingResponse(
static function () use ($eventBus) {
foreach ($eventBus->subscribe() as $event) {
yield "event: {$event->name}\n";
yield "data: " . json_encode($event->payload) . "\n\n";
}
},
headers: [
'Content-Type' => 'text/event-stream',
'Cache-Control' => 'no-cache',
'X-Accel-Buffering' => 'no',
],
);
For interactive realtime, prefer WebSockets. SSE is useful when the client must be a plain browser without a WebSocket client.
Redirects
There is no dedicated redirect helper. Build with status and Location:
// 302 redirect
return Response::ok()
->withStatus(302)
->withHeader('Location', '/orders');
// 301 permanent redirect
return Response::ok()
->withStatus(301)
->withHeader('Location', '/new-path');
Response::created($url) is a 201 with Location — not a redirect, but shares the pattern.
Headers and status
Every PSR-7 method is available on the returned response. PSR-7 immutability rules apply — every with* method returns a new instance:
return JsonResponse::ok(['id' => 42])
->withStatus(201)
->withHeader('X-Trace-Id', $traceId)
->withHeader('Cache-Control', 'no-store')
->withAddedHeader('Vary', 'Authorization');
Asynchronous responses
Handlers can return a Future<ResponseInterface>. The router awaits the future before serialising the response:
use Monadial\Nexus\Runtime\Async\Future;
public function __invoke(ServerRequestInterface $req): Future
{
return $this->orders
->ask(new GetOrder($req->getAttribute('id')), Duration::seconds(2))
->map(static fn($order) => JsonResponse::ok($order->toArray()));
}
This lets you compose async pipelines with map / flatMap and let the router handle the await. See Actors in HTTP for the full pattern.
Custom responses
Both factories return PSR-7 ResponseInterface instances backed by the host's PSR-7 implementation. Produce an entirely custom response with the implementation directly:
use Laminas\Diactoros\Response\TextResponse;
return new TextResponse('plain text body', 200);
See also
- Error Handling — mapping exceptions to responses.
- Actors in HTTP —
Future-returning handlers. - WebSockets —
$ctx->send()for push responses.