Web Frameworks and APIs¶
Modern Perl Web Development¶
Version: 1.1 Year: 2026
Copyright Notice¶
Copyright (c) 2025-2026 Ryan Thomas Robson / Robworks Software LLC. Licensed under CC BY-NC-ND 4.0. You may share this material for non-commercial purposes with attribution, but you may not distribute modified versions.
Perl has a mature web ecosystem that runs everything from small internal APIs to high-traffic production services. Two frameworks dominate modern Perl web development: Mojolicious for full-featured applications and Dancer2 for lightweight microservices. Both sit on top of PSGI, a specification that decouples your application code from the web server, much like Python's WSGI or Ruby's Rack.
This guide covers the foundation layer (PSGI/Plack), the two major frameworks, RESTful API design patterns, and everything you need to go from a hello-world route to a deployed, production-ready web application.
PSGI and Plack¶
Before PSGI, Perl web applications were tied to specific deployment mechanisms - CGI, mod_perl, FastCGI - and switching between them meant rewriting glue code. PSGI (Perl Web Server Gateway Interface) defines a standard interface between web servers and Perl applications. Your application is a code reference that receives a request environment hash and returns a three-element array reference:
my $app = sub {
my ($env) = @_;
return [
200, # HTTP status
['Content-Type' => 'text/plain'], # headers
['Hello, PSGI!'], # body
];
};
Plack is the reference implementation of PSGI. It provides plackup (a development server), middleware components, and adapters for production servers like Starman and Twiggy.
The PSGI Stack¶
flowchart TD
A[HTTP Client] --> B[Web Server / Reverse Proxy]
B --> C[PSGI Server\nStarman / Hypnotoad / Twiggy]
C --> D[Middleware Stack\nPlack::Middleware::*]
D --> E[PSGI Application\nMojolicious / Dancer2 / Raw PSGI]
E --> F[Response]
F --> D
D --> C
C --> B
B --> A
The PSGI environment hash ($env) contains request data:
| Key | Description | Example |
|---|---|---|
REQUEST_METHOD |
HTTP method | GET, POST |
PATH_INFO |
Request path | /api/users/42 |
QUERY_STRING |
URL parameters | page=2&limit=10 |
HTTP_HOST |
Host header | example.com |
CONTENT_TYPE |
Request content type | application/json |
psgi.input |
Request body stream | IO handle |
Mojolicious¶
Mojolicious is a real-time web framework with zero non-core dependencies. It includes an HTTP client, WebSocket support, a template engine, a built-in development server, and a hot-reloading production server called Hypnotoad.
Installation¶
Mojolicious::Lite¶
For small applications and prototyping, Mojolicious::Lite provides a single-file DSL:
use Mojolicious::Lite -signatures;
get '/' => sub ($c) {
$c->render(text => 'Hello, Mojolicious!');
};
get '/greet/:name' => sub ($c) {
my $name = $c->param('name');
$c->render(text => "Hello, $name!");
};
app->start;
Routes and Controllers¶
Routes map HTTP methods and paths to handler code. Beyond simple placeholders, Mojolicious supports route constraints, optional parameters, and nested routes:
get '/user/:id' => [id => qr/\d+/] => sub ($c) {
$c->render(json => {user_id => $c->param('id')});
};
get '/page/:num' => {num => 1} => sub ($c) {
$c->render(text => "Page " . $c->param('num'));
};
For larger applications, move handlers into controller classes:
# lib/MyApp/Controller/Users.pm
package MyApp::Controller::Users;
use Mojo::Base 'Mojolicious::Controller', -signatures;
sub show ($self) {
my $user = $self->app->model->get_user($self->param('id'));
return $self->reply->not_found unless $user;
$self->render(json => $user);
}
1;
Templates¶
Mojolicious includes Embedded Perl (EP) templates:
get '/dashboard' => sub ($c) {
$c->stash(title => 'Dashboard', items => ['Tasks', 'Messages']);
$c->render(template => 'dashboard');
};
__DATA__
@@ dashboard.html.ep
<h1><%= $title %></h1>
<ul>
% for my $item (@$items) {
<li><%= $item %></li>
% }
</ul>
| Tag | Purpose |
|---|---|
<%= expr %> |
Output expression (HTML-escaped) |
<%== expr %> |
Output raw (no escaping) |
% code |
Perl code line |
WebSockets¶
Mojolicious has first-class WebSocket support:
Dancer2¶
Dancer2 is a lightweight framework inspired by Ruby's Sinatra. It emphasizes simplicity and convention over configuration.
Routes and Plugins¶
Dancer2 routes use a concise DSL:
package MyApp;
use Dancer2;
get '/hello/:name' => sub {
my $name = route_parameters->get('name');
return "Hello, $name!";
};
post '/data' => sub {
my $payload = body_parameters->get('key');
return "Received: $payload";
};
The plugin ecosystem provides extensions for databases, authentication, sessions, and templates. Template Toolkit is the most common template engine:
# config.yml: template: "template_toolkit"
get '/user/:id' => sub {
template 'user' => { user => get_user(route_parameters->get('id')) };
};
RESTful API Design¶
Resource-Oriented Routes¶
Map HTTP methods to CRUD operations on resources:
| Method | Path | Action |
|---|---|---|
GET |
/api/tasks |
List all tasks |
POST |
/api/tasks |
Create a task |
GET |
/api/tasks/:id |
Get one task |
PUT |
/api/tasks/:id |
Replace a task |
DELETE |
/api/tasks/:id |
Remove a task |
JSON Handling¶
Mojolicious has built-in JSON support through Mojo::JSON:
post '/api/tasks' => sub ($c) {
my $data = $c->req->json;
return $c->render(json => {error => 'Invalid JSON'}, status => 400)
unless $data;
$c->render(json => create_task($data), status => 201);
};
Dancer2 auto-serializes with Dancer2::Serializer::JSON:
# config.yml: serializer: JSON
post '/api/tasks' => sub {
my $data = request->data; # auto-deserialized
status 201;
return create_task($data); # auto-serialized to JSON
};
Request Lifecycle¶
sequenceDiagram
participant Client
participant Server
participant Middleware
participant Router
participant Controller
participant Model
Client->>Server: HTTP Request
Server->>Middleware: PSGI $env
Middleware->>Middleware: Auth / Logging / CORS
Middleware->>Router: Processed $env
Router->>Controller: Dispatch to handler
Controller->>Model: Business logic / DB query
Model-->>Controller: Data
Controller-->>Middleware: PSGI response
Middleware-->>Server: Final response
Server-->>Client: HTTP Response
Middleware¶
Middleware wraps your application to add cross-cutting concerns - logging, authentication, CORS headers, compression - without cluttering route handlers.
Plack Middleware¶
Plack::Middleware provides a library of reusable components:
use Plack::Builder;
builder {
enable 'AccessLog';
enable 'ContentLength';
enable 'Deflater';
enable 'Static', path => qr{^/static/}, root => './public/';
MyApp->to_app;
};
Framework Hooks¶
Both frameworks provide lifecycle hooks that serve as framework-native middleware:
# Mojolicious
app->hook(before_dispatch => sub ($c) {
$c->app->log->info($c->req->method . ' ' . $c->req->url);
});
# Dancer2
hook before => sub {
if (request->path =~ m{^/api/} && !is_authenticated()) {
send_error('Unauthorized', 401);
}
};
Session Management and Authentication¶
Sessions¶
Mojolicious sessions are cookie-based by default, signed using the application secret:
app->secrets(['change-this-to-a-random-string']);
post '/login' => sub ($c) {
my $user = authenticate($c->req->json);
return $c->render(json => {error => 'Invalid credentials'}, status => 401)
unless $user;
$c->session(user_id => $user->{id});
$c->session(expires => time + 3600);
$c->render(json => {message => 'Logged in'});
};
Dancer2 sessions are configured through config.yml:
post '/login' => sub {
session user_id => authenticate(request->data)->{id};
return {message => 'Logged in'};
};
Token-Based Authentication (JWT)¶
For APIs, JSON Web Tokens avoid server-side session storage. The Mojo::JWT module handles encoding and decoding:
use Mojo::JWT;
my $SECRET = $ENV{JWT_SECRET} || die "JWT_SECRET required\n";
sub generate_token {
my ($user_id) = @_;
return Mojo::JWT->new(
secret => $SECRET,
claims => {sub => $user_id, iat => time},
expires => time + 3600,
)->encode;
}
Authentication Middleware with under¶
Protect API routes with Mojolicious's under, which acts as a route prefix guard:
under '/api' => sub ($c) {
my $auth = $c->req->headers->authorization // '';
my ($token) = $auth =~ /^Bearer\s+(.+)$/;
my $claims = eval { Mojo::JWT->new(secret => $SECRET)->decode($token // '') };
unless ($claims) {
$c->render(json => {error => 'Unauthorized'}, status => 401);
return undef;
}
$c->stash(user_id => $claims->{sub});
return 1;
};
Mojolicious 'under' Routes
under creates a route prefix that acts as middleware. If the handler returns a false value, the request chain stops. This is the idiomatic way to add authentication guards in Mojolicious.
Database Integration¶
DBI Direct¶
DBI is the standard database interface for Perl:
use DBI;
my $dbh = DBI->connect('dbi:Pg:dbname=myapp', 'appuser', 'secret',
{RaiseError => 1, AutoCommit => 1});
get '/api/users' => sub ($c) {
my $users = $dbh->selectall_arrayref(
'SELECT id, name, email FROM users', {Slice => {}});
$c->render(json => $users);
};
Mojo::Pg¶
Mojo::Pg integrates PostgreSQL with Mojolicious, providing connection pooling, migrations, and non-blocking queries:
use Mojo::Pg;
helper pg => sub { state $pg = Mojo::Pg->new($ENV{DATABASE_URL}) };
app->pg->auto_migrate(1)->migrations->from_data;
post '/api/tasks' => sub ($c) {
my $task = $c->pg->db->insert('tasks',
{title => $c->req->json->{title}},
{returning => '*'})->hash;
$c->render(json => $task, status => 201);
};
DBIx::Class¶
DBIx::Class is Perl's ORM with relationship mapping and a Perl-level query interface:
package MyApp::Schema::Result::Task;
use base 'DBIx::Class::Core';
__PACKAGE__->table('tasks');
__PACKAGE__->add_columns(
id => {data_type => 'integer', is_auto_increment => 1},
title => {data_type => 'text', is_nullable => 0},
);
__PACKAGE__->set_primary_key('id');
1;
| Approach | Best For | Trade-off |
|---|---|---|
| DBI direct | Simple queries, full SQL control | Manual SQL, no abstraction |
| Mojo::Pg | Mojolicious apps, PostgreSQL | Postgres-specific |
| DBIx::Class | Complex schemas, relationships | Learning curve, startup overhead |
Deployment¶
Development Servers¶
morbo myapp.pl # Mojolicious - auto-reloads on changes
plackup -R lib/ bin/app.psgi # Dancer2 / any PSGI app
Production Servers¶
Hypnotoad is Mojolicious's preforking server with zero-downtime restarts:
hypnotoad myapp.pl # start
hypnotoad myapp.pl # run again for zero-downtime restart
hypnotoad -s myapp.pl # stop
Starman is a high-performance preforking PSGI server for any framework:
Reverse Proxy¶
Place Nginx in front for TLS termination, static files, and load balancing:
upstream myapp { server 127.0.0.1:8080; }
server {
listen 443 ssl;
server_name example.com;
location /static/ { alias /var/www/myapp/public/; expires 30d; }
location / {
proxy_pass http://myapp;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Enable Proxy Mode
When behind a reverse proxy, enable proxy mode so your app trusts X-Forwarded-For and X-Forwarded-Proto headers. In Mojolicious: proxy => 1 in the hypnotoad config. In Dancer2: behind_proxy: 1 in config.yml.
Docker¶
FROM perl:5.38-slim
RUN cpanm --notest Mojolicious Mojo::Pg
WORKDIR /app
COPY . .
EXPOSE 8080
CMD ["hypnotoad", "-f", "myapp.pl"]
The -f flag keeps Hypnotoad in the foreground, which Docker requires. Pair with docker-compose.yml for database services:
services:
web:
build: .
ports: ["8080:8080"]
environment:
DATABASE_URL: postgresql://user:pass@db:5432/myapp
depends_on: [db]
db:
image: postgres:16
environment: { POSTGRES_USER: user, POSTGRES_PASSWORD: pass, POSTGRES_DB: myapp }
Exercises¶
Scaffolding with mojo generate¶
The mojo generate command creates boilerplate for Mojolicious projects:
Putting It All Together¶
Perl's web ecosystem gives you a clear stack from the protocol layer to the application layer:
- PSGI/Plack provides the universal interface between servers and applications
- Mojolicious is a batteries-included framework with built-in HTTP client, WebSockets, templates, and production server
- Dancer2 is a micro-framework that stays out of your way and lets you choose your components
- RESTful patterns map HTTP methods to CRUD operations on resources with proper status codes
- Middleware adds cross-cutting concerns without coupling them to your route logic
- Sessions and authentication can use cookies, tokens (JWT), or server-side storage
- Database integration ranges from raw DBI to Mojo::Pg to DBIx::Class for full ORM
- Deployment means a reverse proxy (Nginx) in front of a preforking server (Hypnotoad or Starman), optionally containerized with Docker
Start with Mojolicious::Lite for prototyping, graduate to full Mojolicious when you need controllers and models, and reach for Dancer2 when you want a Sinatra-style micro-framework. The PSGI layer means you can mix and match servers without changing application code.
Further Reading¶
- Mojolicious documentation - official guides, cookbook, and API reference
- Mojolicious::Lite tutorial - step-by-step introduction
- Dancer2 documentation - framework reference and plugin list
- Dancer2 manual - comprehensive usage guide
- PSGI/Plack specification - the interface standard
- Mojo::Pg documentation - PostgreSQL integration for Mojolicious
- DBIx::Class manual - ORM documentation
Previous: Networking and Daemons | Next: Developer Roadmap | Back to Index