NAME
PAGI::Cookbook - Recipes for Common PAGI Tasks
DESCRIPTION
This cookbook provides ready-to-use solutions for common web development tasks with PAGI. Each recipe is self-contained and can be adapted to your needs.
Prerequisites: This cookbook assumes you've read the PAGI::Tutorial, particularly Parts 1-3 covering the raw PAGI protocol, middleware, and helper classes.
ROUTING RECIPES
These recipes cover advanced routing patterns using PAGI::App::Router and PAGI::Endpoint::Router. For basic routing, see the Tutorial's Built-in Applications section.
Path Parameters and Wildcards
Use :param syntax for named parameters in routes:
use strict;
use warnings;
use Future::AsyncAwait;
use PAGI::App::Router;
use PAGI::Request;
use PAGI::Response;
my $router = PAGI::App::Router->new;
# Single parameter
$router->get('/users/:id' => async sub {
my ($scope, $receive, $send) = @_;
my $req = PAGI::Request->new($scope, $receive);
my $res = PAGI::Response->new($scope, $send);
# Access path parameter via $req->path_param()
my $id = $req->path_param('id');
await $res->json({ id => $id, name => 'User ' . $id });
});
# Multiple parameters
$router->get('/posts/:post_id/comments/:comment_id' => async sub {
my ($scope, $receive, $send) = @_;
my $req = PAGI::Request->new($scope, $receive);
my $res = PAGI::Response->new($scope, $send);
my $post_id = $req->path_param('post_id');
my $comment_id = $req->path_param('comment_id');
await $res->json({
post_id => $post_id,
comment_id => $comment_id,
});
});
$router->to_app;
Parameters are stored in $scope->{path_params} and accessed via $req->path_param($name).
Wildcard Routes
Use *name syntax to capture multiple path segments:
my $router = PAGI::App::Router->new;
# Wildcard captures everything after /files/
$router->get('/files/*path' => async sub {
my ($scope, $receive, $send) = @_;
my $req = PAGI::Request->new($scope, $receive);
my $res = PAGI::Response->new($scope, $send);
# Access wildcard via $req->path_param()
my $path = $req->path_param('path');
# /files/docs/readme.txt -> path = 'docs/readme.txt'
# /files/images/logo.png -> path = 'images/logo.png'
await $res->text("File path: $path");
});
$router->to_app;
Wildcards match one or more path segments, while named parameters (:param) match a single segment.
WebSocket and SSE Routes
Route WebSocket connections by path:
use strict;
use warnings;
use Future::AsyncAwait;
use PAGI::App::Router;
use PAGI::WebSocket;
my $router = PAGI::App::Router->new;
# WebSocket echo server
$router->websocket('/ws' => async sub {
my ($scope, $receive, $send) = @_;
my $ws = PAGI::WebSocket->new($scope, $receive, $send);
await $ws->accept;
await $ws->each_text(async sub {
my ($text) = @_;
await $ws->send_text("Echo: $text");
});
});
# WebSocket with path parameters
$router->websocket('/ws/chat/:room' => async sub {
my ($scope, $receive, $send) = @_;
my $ws = PAGI::WebSocket->new($scope, $receive, $send);
# Access room parameter
my $room = $ws->stash->{'pagi.params'}{room};
await $ws->accept;
await $ws->send_text("Joined room: $room");
# ... chat logic ...
});
$router->to_app;
SSE Routes
Route Server-Sent Events by path:
use strict;
use warnings;
use Future::AsyncAwait;
use PAGI::App::Router;
use PAGI::SSE;
my $router = PAGI::App::Router->new;
# SSE clock
$router->sse('/events' => async sub {
my ($scope, $receive, $send) = @_;
my $sse = PAGI::SSE->new($scope, $receive, $send);
await $sse->start;
await $sse->every(1, async sub {
await $sse->send_event("Time: " . time);
});
});
# SSE with path parameters
$router->sse('/events/:channel' => async sub {
my ($scope, $receive, $send) = @_;
my $sse = PAGI::SSE->new($scope, $receive, $send);
# Access channel parameter
my $channel = $sse->stash->{'pagi.params'}{channel};
await $sse->start;
await $sse->send_event("Subscribed to: $channel");
# ... streaming logic ...
});
$router->to_app;
Route-Level Middleware
Apply middleware to specific routes by passing an arrayref:
use strict;
use warnings;
use Future::AsyncAwait;
use PAGI::App::Router;
use PAGI::Request;
use PAGI::Response;
my $router = PAGI::App::Router->new;
# Define middleware
my $auth_mw = async sub {
my ($scope, $receive, $send, $next) = @_;
# Check authorization
my $token = '';
for my $h (@{$scope->{headers}}) {
if (lc($h->[0]) eq 'authorization') {
$token = $h->[1];
last;
}
}
unless ($token eq 'Bearer secret123') {
my $res = PAGI::Response->new($scope, $send);
await $res->status(401)->json({ error => 'Unauthorized' });
return; # Don't call $next->()
}
# Authorized - continue to handler
await $next->();
};
# Apply middleware to specific routes
$router->get('/admin' => [$auth_mw] => async sub {
my ($scope, $receive, $send) = @_;
my $res = PAGI::Response->new($scope, $send);
await $res->text('Admin panel');
});
# Multiple middleware
my $log_mw = async sub {
my ($scope, $receive, $send, $next) = @_;
warn "Request: $scope->{method} $scope->{path}\n";
await $next->();
};
$router->post('/admin/users' => [$log_mw, $auth_mw] => async sub {
my ($scope, $receive, $send) = @_;
# ... handler ...
});
$router->to_app;
Middleware are executed in order ($log_mw runs before $auth_mw in the example above).
Nested Routers and Mounting
Use mount() to attach sub-applications or routers at a prefix:
use strict;
use warnings;
use Future::AsyncAwait;
use PAGI::App::Router;
use PAGI::Request;
use PAGI::Response;
# Create API router
my $api_router = PAGI::App::Router->new;
$api_router->get('/users' => async sub {
my ($scope, $receive, $send) = @_;
my $res = PAGI::Response->new($scope, $send);
await $res->json([{ id => 1, name => 'Alice' }]);
});
# Create main router
my $main_router = PAGI::App::Router->new;
# Mount API router at /api prefix
# /api/users will be handled by $api_router
$main_router->mount('/api' => $api_router->to_app);
# Mount with middleware (applies to all sub-routes)
my $cors_mw = async sub {
my ($scope, $receive, $send, $next) = @_;
# Add CORS headers...
await $next->();
};
$main_router->mount('/api' => [$cors_mw] => $api_router->to_app);
$main_router->to_app;
Class-Based Routing with PAGI::Endpoint::Router
PAGI::Endpoint::Router provides an object-oriented approach to routing. You define a class that extends PAGI::Endpoint::Router, implement a routes() method, and use method names as handlers.
Basic Router Class
package MyApp;
use parent 'PAGI::Endpoint::Router';
use strict;
use warnings;
use Future::AsyncAwait;
# Define routes
sub routes {
my ($self, $r) = @_;
$r->get('/' => 'home');
$r->get('/about' => 'about');
$r->post('/contact' => 'contact');
}
# Handler methods receive ($self, $req, $res) for HTTP
async sub home {
my ($self, $req, $res) = @_;
await $res->html('<h1>Welcome!</h1>');
}
async sub about {
my ($self, $req, $res) = @_;
await $res->html('<h1>About Us</h1>');
}
async sub contact {
my ($self, $req, $res) = @_;
my $data = await $req->json;
# ... process contact form ...
await $res->json({ success => 1 });
}
1;
# In app.pl:
# use MyApp;
# MyApp->to_app;
Handler methods are called with:
$self- Router instance (access$self->statefor per-instance data)$req- PAGI::Request object (already constructed)$res- PAGI::Response object (already constructed)
WebSocket Handlers
WebSocket handlers receive ($self, $ws):
package MyApp;
use parent 'PAGI::Endpoint::Router';
use strict;
use warnings;
use Future::AsyncAwait;
sub routes {
my ($self, $r) = @_;
$r->get('/' => 'home');
$r->websocket('/ws' => 'handle_ws');
$r->websocket('/ws/chat/:room' => 'handle_chat');
}
async sub home {
my ($self, $req, $res) = @_;
await $res->html('<h1>Home</h1>');
}
# WebSocket handler receives ($self, $ws)
async sub handle_ws {
my ($self, $ws) = @_;
await $ws->accept;
await $ws->each_text(async sub {
my ($text) = @_;
await $ws->send_text("Echo: $text");
});
}
# WebSocket with path parameters
async sub handle_chat {
my ($self, $ws) = @_;
# Access path parameter via $ws->path_param()
my $room = $ws->path_param('room');
await $ws->accept;
await $ws->send_text("Joined room: $room");
# ... chat logic ...
}
1;
SSE Handlers
SSE handlers receive ($self, $sse):
package MyApp;
use parent 'PAGI::Endpoint::Router';
use strict;
use warnings;
use Future::AsyncAwait;
sub routes {
my ($self, $r) = @_;
$r->get('/' => 'home');
$r->sse('/events' => 'handle_events');
$r->sse('/events/:channel' => 'handle_channel');
}
async sub home {
my ($self, $req, $res) = @_;
await $res->html('<h1>Home</h1>');
}
# SSE handler receives ($self, $sse)
async sub handle_events {
my ($self, $sse) = @_;
await $sse->start;
await $sse->every(1, async sub {
await $sse->send_event("Time: " . time);
});
}
# SSE with path parameters
async sub handle_channel {
my ($self, $sse) = @_;
# Access path parameter
my $channel = $sse->scope->{'pagi.params'}{channel};
await $sse->start;
await $sse->send_event("Channel: $channel");
# ... streaming logic ...
}
1;
Lifecycle Hooks
Override on_startup and on_shutdown for application lifecycle management:
package MyApp;
use parent 'PAGI::Endpoint::Router';
use strict;
use warnings;
use Future::AsyncAwait;
# Called when application starts
async sub on_startup {
my ($self) = @_;
# Initialize resources
$self->state->{db} = DBI->connect('dbi:SQLite:dbname=app.db');
$self->state->{started_at} = time();
warn "Application started\n";
}
# Called when application shuts down
async sub on_shutdown {
my ($self) = @_;
# Clean up resources
$self->state->{db}->disconnect if $self->state->{db};
warn "Application shut down\n";
}
sub routes {
my ($self, $r) = @_;
$r->get('/' => 'home');
}
async sub home {
my ($self, $req, $res) = @_;
# Access state initialized in on_startup
my $db = $self->state->{db};
my $uptime = time() - $self->state->{started_at};
await $res->json({
uptime => $uptime,
database => defined($db) ? 'connected' : 'disconnected',
});
}
1;
Note: $self->state is per-worker in multi-worker mode. For shared state, use an external store (Redis, database, etc.).
Route-Level Middleware with Method Names
Define middleware methods and reference them by name:
package MyApp;
use parent 'PAGI::Endpoint::Router';
use strict;
use warnings;
use Future::AsyncAwait;
sub routes {
my ($self, $r) = @_;
$r->get('/' => 'home');
# Apply middleware by method name
$r->get('/admin' => ['require_auth'] => 'admin_page');
# Multiple middleware
$r->post('/admin/users' => ['log_request', 'require_auth'] => 'create_user');
}
# Middleware method signature: ($self, $req, $res, $next)
async sub require_auth {
my ($self, $req, $res, $next) = @_;
my $token = $req->header('authorization');
unless ($token && $token eq 'Bearer secret123') {
await $res->status(401)->json({ error => 'Unauthorized' });
return; # Don't call $next->()
}
# Authorized - continue to handler
await $next->();
}
async sub log_request {
my ($self, $req, $res, $next) = @_;
warn "Request: " . $req->method . " " . $req->path . "\n";
await $next->();
}
async sub home {
my ($self, $req, $res) = @_;
await $res->html('<h1>Home</h1>');
}
async sub admin_page {
my ($self, $req, $res) = @_;
await $res->html('<h1>Admin Panel</h1>');
}
async sub create_user {
my ($self, $req, $res) = @_;
my $data = await $req->json;
# ... create user ...
await $res->status(201)->json({ success => 1 });
}
1;
Middleware methods receive ($self, $req, $res, $next) and must call await $next->() to continue to the handler.
Mounting Sub-Routers
Create modular applications by mounting sub-routers:
# lib/MyApp/API.pm
package MyApp::API;
use parent 'PAGI::Endpoint::Router';
use strict;
use warnings;
use Future::AsyncAwait;
sub routes {
my ($self, $r) = @_;
$r->get('/users' => 'list_users');
$r->get('/users/:id' => 'get_user');
$r->post('/users' => 'create_user');
}
async sub list_users {
my ($self, $req, $res) = @_;
await $res->json([
{ id => 1, name => 'Alice' },
{ id => 2, name => 'Bob' },
]);
}
async sub get_user {
my ($self, $req, $res) = @_;
my $id = $req->path_param('id');
await $res->json({ id => $id, name => "User $id" });
}
async sub create_user {
my ($self, $req, $res) = @_;
my $data = await $req->json;
await $res->status(201)->json({ id => 3, name => $data->{name} });
}
1;
# lib/MyApp.pm
package MyApp;
use parent 'PAGI::Endpoint::Router';
use strict;
use warnings;
use Future::AsyncAwait;
use MyApp::API;
sub routes {
my ($self, $r) = @_;
$r->get('/' => 'home');
# Mount API router at /api prefix
$r->mount('/api' => MyApp::API->to_app);
}
async sub home {
my ($self, $req, $res) = @_;
await $res->html('<h1>Home</h1>');
}
1;
# app.pl
# use MyApp;
# MyApp->to_app;
Now /api/users and /api/users/:id are handled by MyApp::API.
Complete Example
Here's a complete working example demonstrating all features:
package MyApp;
use parent 'PAGI::Endpoint::Router';
use strict;
use warnings;
use Future::AsyncAwait;
async sub on_startup {
my ($self) = @_;
$self->state->{started} = time();
$self->state->{users} = [
{ id => 1, name => 'Alice' },
{ id => 2, name => 'Bob' },
];
}
sub routes {
my ($self, $r) = @_;
# HTTP routes
$r->get('/' => 'home');
$r->get('/users' => 'list_users');
$r->get('/users/:id' => 'get_user');
$r->post('/users' => 'create_user');
# Protected route
$r->get('/admin' => ['require_auth'] => 'admin');
# WebSocket
$r->websocket('/ws' => 'ws_echo');
# SSE
$r->sse('/events' => 'events');
}
async sub require_auth {
my ($self, $req, $res, $next) = @_;
my $token = $req->header('authorization');
return await $res->status(401)->json({ error => 'Unauthorized' })
unless $token && $token eq 'Bearer secret';
await $next->();
}
async sub home {
my ($self, $req, $res) = @_;
await $res->html('<h1>Welcome to MyApp</h1>');
}
async sub list_users {
my ($self, $req, $res) = @_;
await $res->json($self->state->{users});
}
async sub get_user {
my ($self, $req, $res) = @_;
my $id = $req->path_param('id');
my ($user) = grep { $_->{id} == $id } @{$self->state->{users}};
return await $res->status(404)->json({ error => 'Not found' })
unless $user;
await $res->json($user);
}
async sub create_user {
my ($self, $req, $res) = @_;
my $data = await $req->json;
my $user = { id => scalar(@{$self->state->{users}}) + 1, name => $data->{name} };
push @{$self->state->{users}}, $user;
await $res->status(201)->json($user);
}
async sub admin {
my ($self, $req, $res) = @_;
await $res->json({ message => 'Admin access granted' });
}
async sub ws_echo {
my ($self, $ws) = @_;
await $ws->accept;
await $ws->each_text(async sub {
my ($text) = @_;
await $ws->send_text("Echo: $text");
});
}
async sub events {
my ($self, $sse) = @_;
await $sse->start;
await $sse->every(1, async sub {
await $sse->send_json({ time => time });
});
}
1;
Run it:
# app.pl
use MyApp;
MyApp->to_app;
# Terminal:
pagi-server app.pl --port 5000
Test it:
# HTTP
curl http://localhost:5000/
curl http://localhost:5000/users
curl http://localhost:5000/users/1
curl -X POST http://localhost:5000/users -H 'Content-Type: application/json' -d '{"name":"Charlie"}'
# Protected route
curl http://localhost:5000/admin
curl -H 'Authorization: Bearer secret' http://localhost:5000/admin
# WebSocket (JavaScript)
const ws = new WebSocket('ws://localhost:5000/ws');
ws.onmessage = (e) => console.log(e.data);
ws.send('Hello!');
# SSE (JavaScript)
const sse = new EventSource('http://localhost:5000/events');
sse.onmessage = (e) => console.log(JSON.parse(e.data));
See examples/endpoint-router-demo/ for more examples.
AUTHENTICATION & SESSIONS
Session Management
Use PAGI::Middleware::Session for session management:
use strict;
use warnings;
use Future::AsyncAwait;
use PAGI::App::Router;
use PAGI::Middleware::Session;
use PAGI::Request;
use PAGI::Response;
my $router = PAGI::App::Router->new;
# Add session middleware
$router->use(PAGI::Middleware::Session->new(
secret => 'your-secret-key-here',
cookie_name => 'session_id',
expires => 86400, # 1 day
));
$router->get('/login' => async sub {
my ($scope, $receive, $send) = @_;
my $req = PAGI::Request->new($scope, $receive);
my $res = PAGI::Response->new($scope, $send);
# Set session data
$req->session->{user_id} = 123;
$req->session->{username} = 'alice';
await $res->text('Logged in!');
});
$router->get('/profile' => async sub {
my ($scope, $receive, $send) = @_;
my $req = PAGI::Request->new($scope, $receive);
my $res = PAGI::Response->new($scope, $send);
# Read session data
my $username = $req->session->{username} // 'Guest';
await $res->text("Hello, $username!");
});
$router->get('/logout' => async sub {
my ($scope, $receive, $send) = @_;
my $req = PAGI::Request->new($scope, $receive);
my $res = PAGI::Response->new($scope, $send);
# Clear session
$req->session({});
await $res->text('Logged out!');
});
$router->to_app;
Basic Authentication
use strict;
use warnings;
use Future::AsyncAwait;
use PAGI::App::Router;
use PAGI::Middleware::BasicAuth;
use PAGI::Response;
my $router = PAGI::App::Router->new;
# Protect routes with Basic Auth
$router->use(PAGI::Middleware::BasicAuth->new(
realm => 'Admin Area',
authenticator => sub {
my ($username, $password) = @_;
return $username eq 'admin' && $password eq 'secret';
},
));
$router->get('/' => async sub {
my ($scope, $receive, $send) = @_;
my $res = PAGI::Response->new($scope, $send);
await $res->text('Protected content');
});
$router->to_app;
Bearer Token (JWT)
use strict;
use warnings;
use Future::AsyncAwait;
use PAGI::App::Router;
use PAGI::Middleware::Auth::Bearer;
use PAGI::Request;
use PAGI::Response;
my $router = PAGI::App::Router->new;
# Verify JWT tokens
$router->use(PAGI::Middleware::Auth::Bearer->new(
validate => sub {
my ($token) = @_;
# Return user data if valid, undef if not
return decode_jwt($token); # Your JWT decode logic
},
));
$router->get('/api/me' => async sub {
my ($scope, $receive, $send) = @_;
my $req = PAGI::Request->new($scope, $receive);
my $res = PAGI::Response->new($scope, $send);
# User data from validated token
my $user = $req->stash->{user};
await $res->json({ user => $user });
});
$router->to_app;
REAL-TIME PATTERNS
PubSub for WebSocket Chat
For real-time features with multiple WebSocket clients:
use strict;
use warnings;
use Future::AsyncAwait;
use PAGI::App::Router;
use PAGI::WebSocket;
use PAGI::Server::PubSub;
my $pubsub = PAGI::Server::PubSub->new;
my $router = PAGI::App::Router->new;
$router->websocket('/chat/:room' => async sub {
my ($scope, $receive, $send) = @_;
my $ws = PAGI::WebSocket->new($scope, $receive, $send);
my $room = $scope->{'pagi.params'}{room};
await $ws->accept;
# Subscribe to room
my $sub = $pubsub->subscribe($room, sub {
my ($message) = @_;
$ws->try_send_text($message);
});
# Broadcast received messages
await $ws->each_text(sub {
my ($text) = @_;
$pubsub->publish($room, $text);
});
# Unsubscribe on disconnect
$pubsub->unsubscribe($room, $sub);
});
$router->to_app;
Important: PAGI::Server::PubSub is in-memory only and works within a single process. For multi-worker or multi-server deployments, use Redis pub/sub or a message broker.
See examples/websocket-chat/ for a complete chat application example.
SSE Dashboard
Server-Sent Events are ideal for real-time dashboards:
use strict;
use warnings;
use Future::AsyncAwait;
use PAGI::App::Router;
use PAGI::SSE;
my $router = PAGI::App::Router->new;
$router->get('/events' => async sub {
my ($scope, $receive, $send) = @_;
my $sse = PAGI::SSE->new($scope, $receive, $send);
await $sse->start;
# Enable keepalive to prevent timeout
$sse->keepalive(15);
# Send updates every second
await $sse->every(1, async sub {
await $sse->send_json({
cpu => rand(100),
memory => rand(100),
requests => int(rand(1000)),
timestamp => time(),
}, event => 'metrics');
});
});
$router->to_app;
JavaScript client:
const events = new EventSource('/events');
events.addEventListener('metrics', (e) => {
const data = JSON.parse(e.data);
document.getElementById('cpu').textContent = data.cpu.toFixed(1) + '%';
document.getElementById('memory').textContent = data.memory.toFixed(1) + '%';
});
See examples/sse-dashboard/ for a complete dashboard example.
BACKGROUND WORK
Web requests should respond quickly. Long-running work should be done in the background.
Fire-and-Forget Async I/O
For non-blocking operations (async HTTP calls, async database queries), use ->on_fail() to handle errors, then ->retain() to prevent "lost future" warnings:
use strict;
use warnings;
use Future::AsyncAwait;
use PAGI::App::Router;
use PAGI::Response;
my $router = PAGI::App::Router->new;
# Simulated async email service
async sub send_welcome_email {
my ($email) = @_;
# In reality: await $http_client->post_async($email_api, ...);
await IO::Async::Loop->new->delay_future(after => 2);
warn "Email sent to $email\n";
}
$router->post('/signup' => async sub {
my ($scope, $receive, $send) = @_;
my $res = PAGI::Response->new($scope, $send);
# Respond immediately
await $res->status(201)->json({ status => 'created' });
# Fire-and-forget with error handling
# IMPORTANT: Always add on_fail() before retain() to avoid silent failures
send_welcome_email('user@example.com')
->on_fail(sub { warn "Email send failed: @_" })
->retain();
});
$router->to_app;
Warning: Using ->retain() alone silently swallows errors. Always add ->on_fail() to log or handle failures.
Note: If you're writing middleware or server extensions that inherit from IO::Async::Notifier, prefer $self->adopt_future($f) instead of ->retain(). The adopt_future method properly tracks futures and propagates errors to the notifier's error handling.
CPU-Bound Work in Subprocess
For blocking or CPU-intensive work, use IO::Async::Function to run in a child process:
use strict;
use warnings;
use Future::AsyncAwait;
use IO::Async::Function;
use IO::Async::Loop;
use PAGI::App::Router;
use PAGI::Response;
my $loop = IO::Async::Loop->new;
# Worker runs code in subprocess
my $pdf_worker = IO::Async::Function->new(
code => sub {
my ($data) = @_;
# This blocking code runs in a CHILD PROCESS
# It doesn't block the main event loop
sleep 5; # Simulate heavy work
return "PDF generated for $data";
},
);
$loop->add($pdf_worker);
my $router = PAGI::App::Router->new;
$router->post('/generate-pdf' => async sub {
my ($scope, $receive, $send) = @_;
my $res = PAGI::Response->new($scope, $send);
# Option A: Wait for result (blocks this request only)
my $result = await $pdf_worker->call(args => ['report']);
await $res->json({ result => $result });
# Option B: Fire-and-forget (respond immediately)
# my $f = $pdf_worker->call(args => ['report']);
# $f->on_done(sub { warn "PDF ready: $_[0]\n" });
# $f->on_fail(sub { warn "PDF failed: $_[0]\n" });
# $f->retain();
# await $res->json({ status => 'processing' });
});
$router->to_app;
Warning: IO::Async::Function forks child processes. If running with --workers N, each server worker forks its own Function workers. With 4 server workers and 2 Function workers each, you have 12 processes. For high-volume CPU work, consider a dedicated job queue instead.
See examples/background-tasks/ for complete working examples.
FORMS & UPLOADS
URL-Encoded Forms
use strict;
use warnings;
use Future::AsyncAwait;
use PAGI::Request;
use PAGI::Response;
async sub app {
my ($scope, $receive, $send) = @_;
my $req = PAGI::Request->new($scope, $receive);
my $res = PAGI::Response->new($scope, $send);
if ($req->method eq 'POST') {
my $form = await $req->form;
my $username = $form->{username};
my $password = $form->{password};
# Validate and process...
await $res->redirect('/dashboard');
}
else {
await $res->html(<<'HTML');
<form method="POST">
<input name="username" placeholder="Username">
<input name="password" type="password" placeholder="Password">
<button type="submit">Login</button>
</form>
HTML
}
}
\&app;
File Uploads (Multipart)
use strict;
use warnings;
use Future::AsyncAwait;
use PAGI::Request;
use PAGI::Response;
async sub app {
my ($scope, $receive, $send) = @_;
my $req = PAGI::Request->new($scope, $receive);
my $res = PAGI::Response->new($scope, $send);
if ($req->method eq 'POST') {
my $uploads = await $req->uploads;
for my $upload (@$uploads) {
my $filename = $upload->filename;
my $size = $upload->size;
my $type = $upload->content_type;
# Save to disk
$upload->save_to('/uploads/' . $filename);
# Or read content
# my $content = $upload->content;
}
await $res->json({ uploaded => scalar(@$uploads) });
}
else {
await $res->html(<<'HTML');
<form method="POST" enctype="multipart/form-data">
<input type="file" name="file" multiple>
<button type="submit">Upload</button>
</form>
HTML
}
}
\&app;
See examples/10-forms-and-uploads/ for complete examples.
DEPLOYMENT
TLS/HTTPS
PAGI::Server supports TLS natively:
pagi-server app.pl --port 443 \
--tls-cert /path/to/cert.pem \
--tls-key /path/to/key.pem
For development, generate a self-signed certificate:
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem \
-days 365 -nodes -subj '/CN=localhost'
pagi-server app.pl --port 3443 \
--tls-cert cert.pem --tls-key key.pem
Then access via https://localhost:3443 (browser will warn about self-signed cert).
See examples/11-tls-https/ for more TLS configuration options.
TESTING
Test PAGI apps directly without a running server:
use strict;
use warnings;
use Test2::V0;
use Future::AsyncAwait;
# Load your app
my $app = require './app.pl';
# Test helper to create scope and capture response
async sub test_request {
my (%opts) = @_;
my $scope = {
type => 'http',
method => $opts{method} // 'GET',
path => $opts{path} // '/',
query_string => $opts{query} // '',
headers => $opts{headers} // [],
};
my @body_parts = defined $opts{body} ? ($opts{body}) : ();
my $body_sent = 0;
my $receive = async sub {
if (@body_parts) {
return {
type => 'http.request',
body => shift @body_parts,
more => scalar(@body_parts) > 0,
};
}
return { type => 'http.disconnect' };
};
my %response = (status => undef, headers => [], body => '');
my $send = async sub {
my ($event) = @_;
if ($event->{type} eq 'http.response.start') {
$response{status} = $event->{status};
$response{headers} = $event->{headers};
}
elsif ($event->{type} eq 'http.response.body') {
$response{body} .= $event->{body} // '';
}
};
await $app->($scope, $receive, $send);
return \%response;
}
# Tests
subtest 'GET /' => async sub {
my $res = await test_request(path => '/');
is $res->{status}, 200, 'status is 200';
like $res->{body}, qr/Hello/, 'body contains Hello';
};
subtest 'POST /api/users' => async sub {
my $res = await test_request(
method => 'POST',
path => '/api/users',
headers => [['content-type', 'application/json']],
body => '{"name":"Alice"}',
);
is $res->{status}, 201, 'status is 201';
};
done_testing;
SEE ALSO
PAGI::Tutorial - Learn PAGI from the ground up
PAGI::App::Router - Functional routing
PAGI::Endpoint::Router - Class-based routing
PAGI::Middleware::Session - Session management
PAGI::Server::PubSub - In-memory pub/sub
AUTHOR
PAGI Contributors
COPYRIGHT AND LICENSE
This software is copyright (c) 2025 by the PAGI contributors.
This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.