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, $ctx, $next) = @_;
    my $user = verify_token($ctx->header('Authorization'));
    $ctx->stash->set(user => $user);  # Flows to handler and subrouters!
    await $next->();
}

async sub list_users {
    my ($self, $ctx) = @_;
    my $db = $self->state->{db};                 # Worker state via $self
    my $user = $ctx->stash->get('user');          # Set by middleware
    my $users = $db->get_users;
    await $ctx->response->json($users);
}

async sub get_user {
    my ($self, $ctx) = @_;
    my $id = $ctx->request->path_param('id');    # Route parameter
    await $ctx->response->json({ id => $id });
}

async sub chat_handler {
    my ($self, $ctx) = @_;
    my $ws = $ctx->websocket;
    await $ws->accept;
    await $ws->keepalive(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

  • Context objects - Handlers receive a PAGI::Context with protocol-specific accessors (request/response, websocket, sse)

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

  • Worker-local state - $self->state hashref for storing resources like database connections, accessible via $ctx->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

Per-Request Shared State (PAGI::Stash)

$ctx->stash->set(user => $current_user);

PAGI::Stash provides per-request shared state that is accessible across all handlers, middleware, and subrouters processing the same request.

Middleware A
    sets $ctx->stash->set(user => ...)
        Middleware B
            reads $ctx->stash->get('user')
                Subrouter Handler
                    reads $ctx->stash->get('user')  <-- Still visible!

This enables middleware to pass data downstream:

# Auth middleware
async sub require_auth {
    my ($self, $ctx, $next) = @_;
    my $user = verify_token($ctx->header('Authorization'));
    $ctx->stash->set(user => $user);  # Available to ALL downstream
    await $next->();
}

# Handler in subrouter - sees stash from parent middleware
async sub get_profile {
    my ($self, $ctx) = @_;
    my $user = $ctx->stash->get('user');  # Set by middleware above
    await $ctx->response->json($user);
}

HANDLER SIGNATURES

All handlers receive a PAGI::Context as the second argument. The context subclass depends on route type:

# HTTP routes: get, post, put, patch, delete, head, options
async sub handler ($self, $ctx) { }
# $ctx isa PAGI::Context::HTTP
# $ctx->request, $ctx->response

# WebSocket routes
async sub handler ($self, $ctx) { }
# $ctx isa PAGI::Context::WebSocket
# $ctx->websocket

# SSE routes
async sub handler ($self, $ctx) { }
# $ctx isa PAGI::Context::SSE
# $ctx->sse

# Middleware
async sub middleware ($self, $ctx, $next) { }

METHODS

to_app

my $app = MyRouter->to_app;

Returns a PAGI application coderef. Creates a single instance that persists for the worker lifetime.

context_class

sub context_class { 'MyApp::Context' }

Returns the class name used to construct context objects for handlers. Defaults to 'PAGI::Context'. Override in a subclass to use a custom context class (must be a subclass of PAGI::Context).

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 $ctx->state in context 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. PAGI::Stash data flows through to mounted apps.

SEE ALSO

PAGI::Context, PAGI::Stash, PAGI::App::Router, PAGI::Request, PAGI::Response, PAGI::WebSocket, PAGI::SSE