NAME
PAGI::Endpoint::Router - Class-based router with wrapped handlers
SYNOPSIS
package MyApp;
use parent 'PAGI::Endpoint::Router';
use Future::AsyncAwait;
sub routes {
my ($self, $r) = @_;
# Initialize state (or use PAGI::Lifespan wrapper for startup/shutdown)
$self->state->{db} = DBI->connect(...);
$self->state->{cache} = MyApp::Cache->new;
# HTTP routes with middleware
$r->get('/users' => ['require_auth'] => 'list_users');
$r->get('/users/:id' => 'get_user');
# WebSocket and SSE
$r->websocket('/ws/chat/:room' => 'chat_handler');
$r->sse('/events' => 'events_handler');
# Mount sub-routers
$r->mount('/api' => MyApp::API->to_app);
}
# Middleware sets stash - visible to ALL downstream handlers
async sub require_auth {
my ($self, $req, $res, $next) = @_;
my $user = verify_token($req->bearer_token);
$req->stash->{user} = $user; # Flows to handler and subrouters!
await $next->();
}
async sub list_users {
my ($self, $req, $res) = @_;
my $db = $self->state->{db}; # Worker state via $self
my $user = $req->stash->{user}; # Set by middleware
my $users = $db->get_users;
await $res->json($users);
}
async sub get_user {
my ($self, $req, $res) = @_;
my $id = $req->path_param('id'); # Route parameter
await $res->json({ id => $id });
}
async sub chat_handler {
my ($self, $ws) = @_;
await $ws->accept;
$ws->start_heartbeat(25);
await $ws->each_json(async sub {
my ($data) = @_;
await $ws->send_json({ echo => $data });
});
}
# Wrap with PAGI::Lifespan for startup/shutdown hooks
use PAGI::Lifespan;
my $app = PAGI::Lifespan->new(
startup => async sub ($state) {
$state->{db} = DBI->connect(...);
},
shutdown => async sub ($state) {
$state->{db}->disconnect;
},
app => MyApp->to_app,
)->to_app;
DESCRIPTION
PAGI::Endpoint::Router provides a Starlette/Rails-style class-based approach to building PAGI applications. It combines:
Method-based handlers - Define handlers as class methods
Wrapped objects - Handlers receive
PAGI::Request/PAGI::Responsefor HTTP,PAGI::WebSocketfor WebSocket,PAGI::SSEfor SSEMiddleware as methods - Define middleware that can set stash values visible to all downstream handlers
Worker-local state -
$self->statehashref for storing resources like database connections, accessible via$req->state
For lifecycle management (startup/shutdown hooks), wrap your router with PAGI::Lifespan. This separation allows routers to be freely composable without lifecycle conflicts.
STATE VS STASH
PAGI::Endpoint::Router provides two separate storage mechanisms with different scopes and lifetimes.
state - Worker-Local Instance State
$self->state->{db} = $connection;
The state hashref is attached to the router instance. Use it for resources initialized in on_startup like database connections, cache clients, or configuration.
IMPORTANT: Worker Isolation
In a multi-worker or clustered deployment, each worker process has its own isolated copy of state:
Master Process
fork() --> Worker 1 (own $self->state)
--> Worker 2 (own $self->state)
--> Worker 3 (own $self->state)
Changes to state in one worker do NOT affect other workers. For truly shared application state (counters, sessions, feature flags), use external storage:
Redis - Fast in-memory shared state
Database - Persistent shared state
Memcached - Distributed caching
stash - Per-Request Shared Scratch Space
$req->stash->{user} = $current_user;
The stash lives in the request scope and is shared across ALL handlers, middleware, and subrouters processing the same request.
Middleware A
sets $req->stash->{user}
Middleware B
reads $req->stash->{user}
Subrouter Handler
reads $req->stash->{user} <-- Still visible!
This enables middleware to pass data downstream:
# Auth middleware
async sub require_auth {
my ($self, $req, $res, $next) = @_;
my $user = verify_token($req->header('Authorization'));
$req->stash->{user} = $user; # Available to ALL downstream
await $next->();
}
# Handler in subrouter - sees stash from parent middleware
async sub get_profile {
my ($self, $req, $res) = @_;
my $user = $req->stash->{user}; # Set by middleware above
await $res->json($user);
}
HANDLER SIGNATURES
Handlers receive different wrapped objects based on route type:
# HTTP routes: get, post, put, patch, delete, head, options
async sub handler ($self, $req, $res) { }
# $req = PAGI::Request, $res = PAGI::Response
# WebSocket routes
async sub handler ($self, $ws) { }
# $ws = PAGI::WebSocket
# SSE routes
async sub handler ($self, $sse) { }
# $sse = PAGI::SSE
# Middleware
async sub middleware ($self, $req, $res, $next) { }
METHODS
to_app
my $app = MyRouter->to_app;
Returns a PAGI application coderef. Creates a single instance that persists for the worker lifetime.
state
$self->state->{db} = $connection;
Returns the worker-local state hashref. Initialize resources in the routes method or via PAGI::Lifespan wrapper. Access via $self->state in handlers or $req->state in wrapped objects.
Note: This is NOT shared across workers. See "STATE VS STASH".
routes
sub routes {
my ($self, $r) = @_;
$r->get('/path' => 'handler_method');
}
Override to define routes. The $r parameter is a route builder.
ROUTE BUILDER METHODS
HTTP Methods
$r->get($path => 'handler');
$r->get($path => ['middleware'] => 'handler');
$r->post($path => ...);
$r->put($path => ...);
$r->patch($path => ...);
$r->delete($path => ...);
$r->head($path => ...);
$r->options($path => ...);
websocket
$r->websocket($path => 'handler');
sse
$r->sse($path => 'handler');
mount
$r->mount($prefix => $other_app);
Mount another PAGI app at a prefix. Stash flows through to mounted apps.
SEE ALSO
PAGI::App::Router, PAGI::Request, PAGI::Response, PAGI::WebSocket, PAGI::SSE