NAME
PAGI::App::Router - Unified routing for HTTP, WebSocket, and SSE
SYNOPSIS
use PAGI::App::Router;
my $router = PAGI::App::Router->new;
# HTTP routes (method + path)
$router->get('/users/:id' => $get_user);
$router->post('/users' => $create_user);
$router->delete('/users/:id' => $delete_user);
# Routes with middleware
$router->get('/admin' => [$auth_mw, $log_mw] => $admin_handler);
$router->post('/api/data' => [$rate_limit] => $data_handler);
# WebSocket routes (path only)
$router->websocket('/ws/chat/:room' => $chat_handler);
# SSE routes (path only)
$router->sse('/events/:channel' => $events_handler);
# Mount with middleware (applies to all sub-routes)
$router->mount('/api' => [$auth_mw] => $api_router->to_app);
# Static files as fallback
$router->mount('/' => $static_files);
my $app = $router->to_app; # Handles all scope types
DESCRIPTION
Unified router supporting HTTP, WebSocket, and SSE in a single declarative interface. Routes requests based on scope type first, then path pattern. HTTP routes additionally match on method. Returns 404 for unmatched paths and 405 for unmatched HTTP methods. Lifespan events are automatically ignored.
OPTIONS
not_found- Custom app to handle unmatched routes (all scope types)
METHODS
HTTP Route Methods
$router->get($path => $app);
$router->post($path => $app);
$router->put($path => $app);
$router->patch($path => $app);
$router->delete($path => $app);
$router->head($path => $app);
$router->options($path => $app);
Register a route for the given HTTP method. Returns $self for chaining.
websocket
$router->websocket('/ws/chat/:room' => $chat_handler);
Register a WebSocket route. Matches requests where $scope->{type} is 'websocket'. Path parameters work the same as HTTP routes.
sse
$router->sse('/events/:channel' => $events_handler);
Register an SSE (Server-Sent Events) route. Matches requests where $scope->{type} is 'sse'. Path parameters work the same as HTTP routes.
mount
$router->mount('/api' => $api_app);
$router->mount('/admin' => $admin_router->to_app);
Mount a PAGI app under a path prefix. The mounted app receives requests with the prefix stripped from the path and added to root_path.
When a request for /api/users/42 hits a router with /api mounted:
The mounted app sees
$scope->{path}as/users/42$scope->{root_path}becomes/api(or appends to existing)
Mounts are checked before regular routes. Longer prefixes match first, so /api/v2 takes priority over /api.
Example: Organizing a large application
# API routes
my $api = PAGI::App::Router->new;
$api->get('/users' => $list_users);
$api->get('/users/:id' => $get_user);
$api->post('/users' => $create_user);
# Admin routes
my $admin = PAGI::App::Router->new;
$admin->get('/dashboard' => $dashboard);
$admin->get('/settings' => $settings);
# Main router
my $main = PAGI::App::Router->new;
$main->get('/' => $home);
$main->mount('/api' => $api->to_app);
$main->mount('/admin' => $admin->to_app);
# Resulting routes:
# GET / -> $home
# GET /api/users -> $list_users (path=/users, root_path=/api)
# GET /admin/dashboard -> $dashboard (path=/dashboard, root_path=/admin)
to_app
my $app = $router->to_app;
Returns a PAGI application coderef that dispatches requests.
ROUTE-LEVEL MIDDLEWARE
All route methods accept an optional middleware arrayref before the app:
$router->get('/path' => \@middleware => $app);
$router->post('/path' => \@middleware => $app);
$router->mount('/prefix' => \@middleware => $sub_app);
$router->websocket('/ws' => \@middleware => $handler);
$router->sse('/events' => \@middleware => $handler);
Middleware Types
PAGI::Middleware instance
Any object with a
call($scope, $receive, $send, $app)method:use PAGI::Middleware::RateLimit; my $rate_limit = PAGI::Middleware::RateLimit->new(limit => 100); $router->get('/api/data' => [$rate_limit] => $handler);Coderef with $next signature
my $timing = async sub ($scope, $receive, $send, $next) { my $start = time; await $next->(); # Call next middleware or app warn sprintf "Request took %.3fs", time - $start; }; $router->get('/api/data' => [$timing] => $handler);
Execution Order
Middleware executes in array order for requests, reverse order for responses (onion model):
$router->get('/' => [$mw1, $mw2, $mw3] => $app);
# Request flow: mw1 -> mw2 -> mw3 -> app
# Response flow: mw1 <- mw2 <- mw3 <- app
Short-Circuiting
Middleware can skip calling $next to short-circuit the chain:
my $auth = async sub ($scope, $receive, $send, $next) {
unless ($scope->{user}) {
await $send->({
type => 'http.response.start',
status => 401,
headers => [['content-type', 'text/plain']],
});
await $send->({
type => 'http.response.body',
body => 'Unauthorized',
});
return; # Don't call $next
}
await $next->();
};
Stacking with Mount
Mount middleware runs before any sub-router middleware:
my $api = PAGI::App::Router->new;
$api->get('/users' => [$rate_limit] => $list_users);
$router->mount('/api' => [$auth] => $api->to_app);
# Request to /api/users runs: $auth -> $rate_limit -> $list_users
PATH PATTERNS
/users/:id- Named parameter, captured asparams->{id}/files/*path- Wildcard, captures rest of path asparams->{path}
SCOPE ADDITIONS
The router adds the following to scope when a route matches:
$scope->{path_params} # Captured path parameters (hashref)
$scope->{'pagi.router'}{route} # Matched route pattern
Path parameters use a router-agnostic key (path_params) so that PAGI::Request, PAGI::Response, PAGI::WebSocket, and PAGI::SSE can access them via ->path_param('name') regardless of which router implementation populated them.
For mounted apps, root_path is updated to include the mount prefix.