Skip to content

Web Frameworks and APIs

Modern Perl Web Development

Version: 1.1 Year: 2026


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.

plackup app.psgi                                       # development
plackup -s Starman --workers 4 --port 5000 app.psgi    # production

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

cpanm Mojolicious

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:

websocket '/ws' => sub ($c) {
    $c->on(message => sub ($c, $msg) {
        $c->send("Echo: $msg");
    });
};

Dancer2

Dancer2 is a lightweight framework inspired by Ruby's Sinatra. It emphasizes simplicity and convention over configuration.

cpanm Dancer2
dancer2 gen -a MyApp

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:

session: YAML    # or Cookie, Memcached, Redis
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
app->config(hypnotoad => {
    listen  => ['http://*:8080'],
    workers => 4,
    proxy   => 1,
});

Starman is a high-performance preforking PSGI server for any framework:

starman --workers 4 --port 5000 bin/app.psgi

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


Previous: Networking and Daemons | Next: Developer Roadmap | Back to Index

Comments