Routing

Routes are declared with a keyword per method and compiled into a frozen table at to_app. Dispatch is a hash lookup for literal paths and a short bucket scan for parameterised ones.

Routes

get  '/books/:id' => 'Web::Book#view';
any  '/ping'      => sub { my ($c) = @_; $c->text('pong') };
post '/books'     => 'Web::Book#create';

get, post, put, patch, del and any each take a path and a target. The target is a coderef, or a 'Controller#method' string resolved against MyApp::Controller:: at boot - typos croak before the app serves.

Captures

:name captures one path segment, *name captures the rest:

get '/books/:id'   => sub { $_[0]->text('book ' . $_[0]->param('id')) };
get '/files/*path' => sub { $_[0]->text($_[0]->param('path')) };

Captures are available as $c->param($name), alongside query and form parameters - see the context's parameter helpers.

Guard scopes

under opens a scope guarded by a coderef. Guards receive the context; a reference return short-circuits the request, anything else continues. Scopes nest, and each nested scope inherits its parents' guards:

my $admin = under '/admin' => sub {
    my ($c) = @_;
    return $c->redirect('/') unless $c->session->{is_admin};
    return;                        # fall through: carry on
};

$admin->get('/books'  => 'Admin::Book#list');
$admin->post('/books' => 'Admin::Book#create');

At request time the guard chain is a frozen array walk - no dispatch logic runs to find them.

Hooks

hook before_dispatch => sub { my ($c) = @_; ...; return };
hook after_dispatch  => sub { my ($c, $resp) = @_; ... };

before_dispatch runs after routing and before guards; a reference return short-circuits. after_dispatch sees the finalized PSGI triplet and may mutate it or return a replacement.

Errors

on_error sub {
    my ($c, $err) = @_;
    return $c->json({ oops => 1 }, 500);
};

Runs when a guard or handler dies; a reference return becomes the response, otherwise the default 500 {"errors":[...]} is served.

Mounts

Whole applications mount under a prefix, longest prefix first:

static   'https://fal.aihtbprolstatic-tp.hcv9jop5ns4r.cn' => 'root/static';
markdown '/docs'   => 'docs', title => 'MyApp Guide';
mount    '/legacy' => $psgi_app;

A literal route still wins over a mount; parameterised routes lose to a mount prefix. See OpenAPI for the api mount and the markdown-site recipe for markdown.

Special route kinds

websocket and sse routes sit in the same table, under the same scopes and guards - a guard can reject a client with an ordinary HTTP response before any upgrade happens. See WebSockets and SSE.