=pod

=encoding utf-8

=head1 NAME

PAGI::Cookbook - Recipes for Common PAGI Tasks

=head1 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.

B<Prerequisites:> This cookbook assumes you've read the L<PAGI::Tutorial>,
particularly Parts 1-3 covering the raw PAGI protocol, middleware, and helper
classes.

=head1 ROUTING RECIPES

These recipes cover advanced routing patterns using L<PAGI::App::Router> and
L<PAGI::Endpoint::Router>. For basic routing, see the Tutorial's Built-in
Applications section.

=head2 Path Parameters and Wildcards

Use C<: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 C<$scope-E<gt>{path_params}> and accessed via C<$req-E<gt>path_param($name)>.

=head3 Wildcard Routes

Use C<*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 (C<:param>) match a single segment.

=head2 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;

=head3 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 stream
  $router->sse('/events' => async sub {
      my ($scope, $receive, $send) = @_;
      my $sse = PAGI::SSE->new($scope, $receive, $send);

      await $sse->keepalive(30);  # Prevent proxy timeouts
      await $sse->send_event("Connected at: " . time);
      await $sse->run;  # Wait for disconnect
  });

  # 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;

=head2 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 (C<$log_mw> runs before C<$auth_mw> in the example above).

=head2 Nested Routers and Mounting

Use C<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;

=head2 Class-Based Routing with PAGI::Endpoint::Router

L<PAGI::Endpoint::Router> provides an object-oriented approach to routing. You define a class that extends C<PAGI::Endpoint::Router>, implement a C<routes()> method, and use method names as handlers.

=head3 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:

=over 4

=item * C<$self> - Router instance (access C<$self-E<gt>state> for per-instance data)

=item * C<$req> - L<PAGI::Request> object (already constructed)

=item * C<$res> - L<PAGI::Response> object (already constructed)

=back

=head3 WebSocket Handlers

WebSocket handlers receive C<($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;

=head3 SSE Handlers

SSE handlers receive C<($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->keepalive(30);
      await $sse->send_event("Connected at: " . time);
      await $sse->run;
  }

  # 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;

=head3 Lifecycle Hooks

Override C<on_startup> and C<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: C<$self-E<gt>state> is per-worker in multi-worker mode. For shared state, use an external store (Redis, database, etc.).

=head3 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 C<($self, $req, $res, $next)> and must call C<await $next-E<gt>()> to continue to the handler.

=head3 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 C</api/users> and C</api/users/:id> are handled by C<MyApp::API>.

=head3 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->keepalive(30);
      await $sse->send_json({ connected => time });
      await $sse->run;
  }

  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 C<examples/endpoint-router-demo/> for more examples.

=head1 AUTHENTICATION & SESSIONS

=head2 Session Management

Use L<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;

=head2 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;

=head2 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;

=head1 REAL-TIME PATTERNS

=head2 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;

  # Simple in-memory room tracking
  my %rooms;  # room => { client_id => $ws }
  my $next_id = 1;

  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};
      my $client_id = $next_id++;

      await $ws->accept;

      # Keep connection alive through proxies/NAT (protocol-level ping every 25s)
      await $ws->keepalive(25);

      # Join room
      $rooms{$room}{$client_id} = $ws;

      # Broadcast received messages to all clients in room
      await $ws->each_text(sub {
          my ($text) = @_;
          for my $id (keys %{$rooms{$room}}) {
              next if $id == $client_id;  # Don't echo back to sender
              $rooms{$room}{$id}->try_send_text($text);
          }
      });

      # Leave room on disconnect
      delete $rooms{$room}{$client_id};
      delete $rooms{$room} unless keys %{$rooms{$room}};
  });

  $router->to_app;

B<Important:> This in-memory approach works within a single process. For
multi-worker or multi-server deployments, use Redis pub/sub or a message broker.

See L<PAGI::App::WebSocket::Chat> for a full-featured chat application.

=head2 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->sse('/events' => async sub {
      my ($scope, $receive, $send) = @_;

      my $sse = PAGI::SSE->new($scope, $receive, $send);

      # Enable keepalive to prevent proxy timeout
      await $sse->keepalive(15);

      # Subscribe to metrics updates (your pub/sub system)
      my $sub_id = subscribe_metrics(sub {
          my ($metrics) = @_;
          $sse->try_send_event(
              event => 'metrics',
              data  => $metrics,
          );
      });

      $sse->on_close(sub { unsubscribe_metrics($sub_id) });
      await $sse->run;
  });

  $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 C<examples/sse-dashboard/> for a complete dashboard example.

=head1 BACKGROUND WORK

Web requests should respond quickly. Long-running work should be done in the background.

=head2 Fire-and-Forget Async I/O

For non-blocking operations (async HTTP calls, async database queries),
use C<< ->on_fail() >> to handle errors, then C<< ->retain() >> to prevent
"lost future" warnings:

  use strict;
  use warnings;
  use Future::AsyncAwait;
  use Future::IO;
  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 Future::IO->sleep(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;

B<Warning:> C<< ->retain() >> alone silently swallows errors - always add
C<< ->on_fail() >> first.

B<Middleware authors:> If your middleware inherits from L<IO::Async::Notifier>,
use C<< $self->adopt_future($f) >> instead. It properly tracks futures and
propagates unhandled errors to the notifier's C<invoke_error> method.

=head2 CPU-Bound Work in Subprocess

B<Note:> This pattern uses L<IO::Async::Function>, which is specific to
L<PAGI::Server>'s L<IO::Async> backend. It won't work with other PAGI servers.

For blocking or CPU-intensive work, use L<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;

B<Warning:> IO::Async::Function forks child processes. If running with C<--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 C<examples/background-tasks/> for complete working examples.

=head1 FORMS & UPLOADS

=head2 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_params;
          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;

=head2 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 (blocking I/O)
              $upload->move_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 C<examples/10-forms-and-uploads/> for complete examples.

=head1 COMMAND-LINE RECIPES

=head2 Environment Modes

pagi-server auto-detects development vs production mode based on whether
you're running in an interactive terminal:

  # Development mode (TTY detected) - Lint middleware + access logging
  pagi-server app.pl

  # Production mode (no TTY, e.g., systemd/docker) - no middleware, no logging
  # This happens automatically when running as a daemon

  # Explicit mode selection
  pagi-server -E production app.pl
  pagi-server -E development app.pl
  pagi-server -E none app.pl  # Disable all auto-middleware

  # Via environment variable
  PAGI_ENV=production pagi-server app.pl

  # Enable logging in production (writes to file)
  pagi-server -E production --access-log /var/log/pagi.log app.pl

  # Disable auto-middleware even in development
  pagi-server --no-default-middleware app.pl

=head2 Inline Apps with -e

Like C<perl -e>, you can run inline PAGI apps. This is useful for quick
tests or one-liners:

  # Quick static file server
  pagi-server -MPAGI::App::Directory \
      -e 'PAGI::App::Directory->new(root => "/var/www")->to_app'

  # Serve a single file
  pagi-server -MPAGI::App::File \
      -e 'PAGI::App::File->new(root => ".")->to_app'

  # Quick redirect
  pagi-server -MPAGI::App::Redirect \
      -e 'PAGI::App::Redirect->new(location => "https://example.com")->to_app'

=head2 Loading Modules with -M

The C<-M> flag loads modules before C<-e> code runs, like C<perl -M>:

  # Load multiple modules
  pagi-server -MPAGI::App::File -MPAGI::Middleware::Builder \
      -e 'use PAGI::Middleware::Builder; builder { enable "GZip"; PAGI::App::File->new(root => ".")->to_app }'

  # Cuddled form (no space after -M)
  pagi-server -MPAGI::App::File -e 'PAGI::App::File->new(root => ".")->to_app'

=head2 Common pagi-server Patterns

  # Development with verbose logging
  pagi-server --log-level debug app.pl

  # Production multi-worker deployment
  pagi-server -E production --host 0.0.0.0 -p 80 --workers 4 app.pl

  # With custom library paths
  pagi-server -I ./lib -I ./local/lib app.pl

  # HTTPS with TLS
  pagi-server --ssl-cert cert.pem --ssl-key key.pem -p 443 app.pl

  # Daemonized with PID file
  pagi-server -D --pid /var/run/pagi.pid app.pl

  # Maximum performance (no access logging)
  pagi-server --no-access-log --workers 8 -E production app.pl

  # Using EV event loop for better performance
  pagi-server --loop EV app.pl

=head1 DEPLOYMENT

=head2 TLS/HTTPS

L<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 C<https://localhost:3443> (browser will warn about self-signed cert).

See C<examples/11-tls-https/> for more TLS configuration options.

=head2 Serving Large Files with X-Sendfile

For production deployments behind a reverse proxy (Nginx, Apache), use
L<PAGI::Middleware::XSendfile> to delegate file serving to the proxy.
This frees your application worker immediately while the proxy handles
the file transfer using optimized kernel sendfile.

=head3 Why X-Sendfile?

Direct file serving from your application ties up a worker process for
the entire transfer. With slow clients or large files, this limits your
concurrency. X-Sendfile lets your app send a header telling the proxy
to serve the file:

  # Your app sends:
  X-Accel-Redirect: /protected/files/large.bin
  Content-Type: application/octet-stream

  # Nginx intercepts and serves the file directly

Benefits:

=over 4

=item * Worker freed immediately after sending headers

=item * Proxy uses kernel sendfile (zero-copy, efficient)

=item * Proxy handles Range requests, caching, connection management

=item * Slow clients don't block your app

=back

=head3 Nginx Configuration

Configure an internal location for protected files:

  # nginx.conf
  location /protected/ {
      internal;                       # Only accessible via X-Accel-Redirect
      alias /var/www/files/;          # Actual file location
  }

=head3 Application Setup

  use strict;
  use warnings;
  use Future::AsyncAwait;
  use PAGI::Middleware::Builder;
  use PAGI::App::File;

  my $app = builder {
      enable 'XSendfile',
          type    => 'X-Accel-Redirect',
          mapping => { '/var/www/files/' => '/protected/' };

      PAGI::App::File->new(
          root          => '/var/www/files',
          handle_ranges => 0,  # Let nginx handle Range requests
      )->to_app;
  };

B<Note:> Setting C<handle_ranges =E<gt> 0> is important. It tells your app
to ignore Range headers and always send full files. The reverse proxy will
handle Range requests more efficiently using its native sendfile.

=head3 Custom Download Handler

For more control (authentication, logging, custom headers):

  use PAGI::App::Router;
  use PAGI::Request;
  use PAGI::Response;

  my $router = PAGI::App::Router->new;

  $router->get('/download/:filename' => async sub {
      my ($scope, $receive, $send) = @_;
      my $req = PAGI::Request->new($scope, $receive);
      my $res = PAGI::Response->new($scope, $send);

      my $filename = $req->path_param('filename');
      my $filepath = "/var/www/files/$filename";

      # Validate file exists and user has access
      return await $res->status(404)->text('Not found')
          unless -f $filepath;

      # Send file response (XSendfile middleware will intercept)
      await $send->({
          type    => 'http.response.start',
          status  => 200,
          headers => [
              ['content-type', 'application/octet-stream'],
              ['content-disposition', qq{attachment; filename="$filename"}],
          ],
      });
      await $send->({
          type => 'http.response.body',
          file => $filepath,
      });
  });

  my $app = builder {
      enable 'XSendfile',
          type    => 'X-Accel-Redirect',
          mapping => { '/var/www/files/' => '/protected/' };
      $router->to_app;
  };

=head3 Apache Configuration (mod_xsendfile)

Enable mod_xsendfile and whitelist your file directory:

  # Apache config
  XSendFile On
  XSendFilePath /var/www/files

  # Application
  my $app = builder {
      enable 'XSendfile', type => 'X-Sendfile';
      $my_app;
  };

=head3 Filehandle Support

The middleware also works with filehandle responses, but only if the
filehandle object has a C<path()> method. L<IO::File::WithPath> from CPAN
is an easy drop-in for this:

  use IO::File::WithPath;

  # This works - IO::File::WithPath provides the path method
  my $fh = IO::File::WithPath->new('/path/to/file.bin', 'r');
  await $send->({ type => 'http.response.body', fh => $fh });

  # This does NOT trigger X-Sendfile (no path method)
  open my $plain_fh, '<', '/path/to/file.bin';
  await $send->({ type => 'http.response.body', fh => $plain_fh });

For plain filehandles, either use C<file =E<gt> $path> directly, or use
L<IO::File::WithPath>. See L<PAGI::Middleware::XSendfile> for details.

=head1 TESTING

Use L<PAGI::Test::Client> to test apps directly without a running server:

  use strict;
  use warnings;
  use Test2::V0;
  use PAGI::Test::Client;

  # Load your app
  my $app = require './app.pl';
  my $client = PAGI::Test::Client->new(app => $app);

  subtest 'GET /' => sub {
      my $res = $client->get('/');
      is $res->status, 200, 'status is 200';
      like $res->text, qr/Hello/, 'body contains Hello';
  };

  subtest 'POST /api/users' => sub {
      my $res = $client->post('/api/users',
          json => { name => 'Alice' },
      );
      is $res->status, 201, 'status is 201';
      is $res->json->{id}, 1, 'returns user id';
  };

  subtest 'form submission' => sub {
      my $res = $client->post('/login',
          form => { user => 'admin', pass => 'secret' },
      );
      is $res->status, 302, 'redirects after login';

      # Session cookies persist across requests
      my $dashboard = $client->get('/dashboard');
      is $dashboard->status, 200, 'authenticated access';
  };

  subtest 'custom headers' => sub {
      my $res = $client->get('/api/data',
          headers => { Authorization => 'Bearer token123' },
      );
      is $res->status, 200;
  };

  done_testing;

See L<PAGI::Test::Client> for the full API including WebSocket and SSE testing.

=head1 SEE ALSO

=over 4

=item * L<PAGI::Tutorial> - Learn PAGI from the ground up

=item * L<PAGI::App::Router> - Functional routing

=item * L<PAGI::Endpoint::Router> - Class-based routing

=item * L<PAGI::Middleware::Session> - Session management

=item * L<PAGI::App::WebSocket::Chat> - Multi-room chat application

=item * L<PAGI::Middleware::XSendfile> - Delegate file serving to reverse proxy

=item * L<PAGI::Test::Client> - Test apps without a running server

=back

=head1 AUTHOR

PAGI Contributors

=head1 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.

=cut
