Dancer2 + Dancer2::Plugin::DBIC::Async + HTMX

Thursday, Feb 5, 2026| Tags: perl

DISCLAIMER: Image is generated using ChatGPT.



As you all know, I released a significant update (v0.50) to DBIx::Class::Async. The latest version included some breaking changes, which means I needed to revisit its plugin, Dancer2::Plugin::DBIC::Async. I’ve applied the necessary patches to ensure compatibility with the latest DBIx::Class::Async release. The updated plugin is now available as version v0.05.

Now that I have both tools, DBIx::Class::Async and Dancer2::Plugin::DBIC::Async working in harmony, I wanted to put them to use. Recently I came across the term HTMX and began exploring what it offers. As someone who leans more toward backend development and only touches frontend when necessary, HTMX caught my attention.

I decided to build something that brings all these pieces together, DBIX::Class::Async for asynchronous database operations, Dancer2::Plugin::DBIC::Async for seamless integration with Dancer2, and HTMX for a modern, lightweight frontend experience. It’s a chance to see how these tools behave in a real-world scenario.

This is a modern, high-performance Perl web application demonstrating how to build a reactive user interface without a complex JavaScript framework.


Key Features



1. Non-Blocking DB Operations

Uses IO::Async and Dancer2::Plugin::DBIC::Async to handle database queries without stalling the server.


2. Reactive UI

Powered by HTMX for seamless, partial page updates (No full page reloads!).


3. Parallelism

Executes search and count queries simultaneously using Future->wait_all.


4. Clean API

Returns deflated HashRefs, making data handling simple and fast.


Prerequisites



Ensure you have the following dependencies:


$ cpanm -vS Dancer2
$ cpanm -vS DBIx::Class::Async
$ cpanm -vS Dancer2::Plugin::DBIC::Async

Project Structure


The project looks like this:


    ├── app.pl
    ├── bin
    │   └── deploy.pl
    ├── config.yml
    ├── contacts.db
    ├── lib
    │   └── MyApp
    │       ├── Schema
    │       │   └── Result
    │       │       └── Contact.pm
    │       └── Schema.pm
    └── views
        └── index.tt

config.yml: Application configuration file.
app.pl: The Dancer2 application logic and routes.
lib/: Contains your DBIx::Class schema.
views/index.tt: The main dashboard containing the HTMX attributes.
contacts.db: SQLite database.

Quick Start



1. Database Setup


Create your SQLite database and deploy the schema:


$ perl bin/deploy.pl

2. Configuration


Ensure your config.yml points to the correct schema:


logger: "console"
log: "core"
show_errors: 1
startup_info: 1
template: "template_toolkit"

# Plugin Settings
plugins:
    "DBIC::Async":
    default:
        schema_class: "MyApp::Schema"
        dsn: "dbi:SQLite:dbname=contacts.db"
        async:
            workers: 4

3. Template


<!DOCTYPE html>
<html>
<head>
    <title>Async Contact Manager</title>
    <script src="https://unpkg.com/htmx.org@1.9.10"></script>
</head>
<body>
    <h1>Contact Manager</h1>

    <form hx-post="/contacts"
      hx-target="#contact-list"
      hx-swap="innerHTML"
      onaddcontact="this.reset()">
        <input type="text" name="name" placeholder="Name" required>
        <input type="email" name="email" placeholder="Email" required>
        <button type="submit">Add Contact</button>
    </form>

    <hr>

    <div class="search-box">
        <input type="text"
            name="q"
            placeholder="Search contacts..."
            hx-get="/contacts"
            hx-target="#contact-list"
            hx-trigger="keyup changed delay:500ms"
            hx-indicator=".loader">
        <span class="loader htmx-indicator">Searching...</span>
    </div>

    <h2>Active Contacts (<span id="total-count">0</span>)</h2>
    <div id="contact-list" hx-get="/contacts" hx-trigger="load">
        Loading contacts...
    </div>
</body>
</html>

4. Application


#!/usr/bin/env perl

use strict;
use warnings;

use lib 'lib';

use Dancer2;
use Dancer2::Plugin::DBIC::Async;

get '/' => sub {
    template 'index';
};

post '/contacts' => sub {
    my $name  = body_parameters->get('name');
    my $email = body_parameters->get('email');

    async_create('Contact', { name => $name, email => $email }, 'default')->get;

    redirect '/contacts';
};

get '/contacts' => sub {
    my $query = query_parameters->get('q');
    my $cond  = $query ? { name => { -like => "%$query%" } } : {};

    my $search_f = async_search('Contact', $cond, 'default');
    my $count_f  = async_count('Contact', 'default');

    Future->wait_all($search_f, $count_f)->get;

    my $contacts = $search_f->get;
    my $total    = $count_f->get;

    my $html = '<ul class="list-group shadow-sm">';
    if (@$contacts) {
        foreach my $c (@$contacts) {
            $html .= sprintf(
                '<li class="list-group-item d-flex justify-content-between align-items-center">
                    <div>
                        <h6 class="my-0">%s</h6>
                        <small class="text-muted">%s</small>
                    </div>
                </li>',
                $c->{name},
                $c->{email}
            );
        }
    } else {
        $html .= '<li class="list-group-item text-muted text-center py-4">No contacts found</li>';
    }
    $html .= '</ul>';

    $html .= qq|<span id="total-count" hx-swap-oob="innerHTML">$total</span>|;

    content_type 'text/html';
    return $html;
};

dance;

5. Launch the App


perl app.pl

Visit http://localhost:3000 in your browser.


How it Works



The Frontend (HTMX)


The search bar uses hx-get to trigger a request to the server as you type. It waits 500ms after your last keystroke to avoid overloading the DB.


<input type="text" name="q"
       hx-get="/contacts"
       hx-trigger="keyup changed delay:500ms"
       hx-target="#contact-list"
       placeholder="Search contacts...">

The Backend (Asynchronous Perl)


When the request hits /contacts, the server fires two database queries in parallel.


get '/contacts' => sub {
    my $query = query_parameters->get('q');
    my $cond  = $query ? { name => { -like => "%$query%" } } : {};

    my $search_f = async_search('Contact', $cond, 'default');
    my $count_f  = async_count('Contact', 'default');

    Future->wait_all($search_f, $count_f)->get;

    my $contacts = $search_f->get;
    my $total    = $count_f->get;

    #
    # prepare the html contents
    #

    content_type 'text/html';
    return $html;
};

The complete source code is available here: https://github.com/manwar/Dancer2-DBIC-Async-HTMX

So what are you waiting for, go and play.



Happy Hacking !!!

SO WHAT DO YOU THINK ?

If you have any suggestions or ideas then please do share with us.

Contact with me