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::Response for HTTP, PAGI::WebSocket for WebSocket, PAGI::SSE for SSE

  • Middleware as methods - Define middleware that can set stash values visible to all downstream handlers

  • Worker-local state - $self->state hashref 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