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 !!!