I’ve been eager to dive into HTMX, especially after watching the DjangoCon Europe 2022 talk From React to HTMX on a Real-World SaaS Project. I have been using HTMX and Alpine in my day-to-day work lately, and together, they’ve made developing interactive web apps a lot more enjoyable, especially as someone who isn’t particularly fond of front-end development.
I haven’t been a huge fan of Django templates, but HTMX, Alpine.js, and Django Cotton have started to shift my perspective. And with Tailwind CSS on top, it’s become one of the best stacks I’ve worked with so far.
This project is a personal finance management app that integrates Plaid to link bank accounts and provide financial insights. I used Plaid’s official examples from their repository as a reference.
Want a quick overview of the code? Check it out on GitHub
Features
- Plaid Integration to link bank accounts, fetch real-time account data, and track transactions.
- Automated webhooks to:
- Automatically sync and update bank data whenever new transactions are available.
- Easily update and re-authenticate accounts when they enter a bad state (e.g., login required).
- Automatically detect new bank accounts and prompt users to add newly detected accounts.
- Provides financial insights such as net worth, account-specific details, and category-based spending breakdowns.
- Two-factor authentication (2FA) with an authenticator app for an added layer of security.
Dependency Management with Poetry
For managing dependencies in this project, I used Poetry. Poetry simplifies the package management process and automates much of the heavy lifting involved with dependencies. It relies on the pyproject.toml
file, which is now the standard for defining build requirements in modern Python projects.
Why Poetry?
- Easier dependency resolution.
- It creates and manages virtual environments, so if you ever forget to create/activate one, Poetry's got your back.
- It locks down the exact versions of packages, making sure the environment is reproducible. This is very helpful especially when working in a larger team.
Ruff and Pre-commit
I’ve been using Ruff, which is a very fast Python linter and code formatter, built in Rust. It handles a bunch of tools all in one, like Black, isort, and more.
What I like about Ruff:
- You can configure it using
pyproject.toml
, which keeps things clean and centralized. - It’s all-in-one—so no need to manage separate tools like Black, isort, etc.
I also used djlint for linting Django templates. It’s decent, but I’m still on the lookout for something better. If you know of any good alternatives, feel free to share!
For more on setting up Pre-commit hooks for code formatting and linting in Django, check out my article here.
Project Structure
django_finance/
├── apps/
│ ├── accounts/
│ ├── common/
│ └── plaid/
├── config/
│ ├── app_template/
│ ├── settings/
│ ├── asgi.py
│ ├── celery.py
│ ├── urls.py
│ └── wsgi.py
├── static/
│ ├── css/
│ ├── images/
│ └── js/
├── templates/
│ ├── account/
│ ├── components/
│ ├── errors/
│ ├── layouts/
│ ├── mfa/
│ ├── plaid/
│ ├── _base_dashboard.html
│ ├── _base.html
│ └── index.html
├── tests/
│ ├── accounts/
│ ├── common/
│ ├── plaid/
│ └── conftest.py
├── theme/
├── .pre-commit-config.yaml
├── db.sqlite3
├── manage.py
├── poetry.lock
├── pyproject.toml
└── README.md
In the common
app, I add management commands, validation, base models, template tags, etc. The BaseModel
looks like this:
class BaseModel(models.Model):
"""
Base model for common fields and methods.
"""
id = models.AutoField(primary_key=True)
uuid = models.UUIDField(default=uuid.uuid4, unique=True, editable=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
is_active = models.BooleanField(default=True, db_index=True, help_text="Used for soft deleting records.")
objects = BaseModelManager()
class Meta:
abstract = True
def soft_delete(self):
self.is_active = False
self.save()
def restore(self):
self.is_active = True
self.save()
I have also created a custom startapp
command to generate Django apps with predefined files and content suitable for my use case.
Tests
For writing tests, I use pytest
alongside factory_boy
, which makes it easy to declare test-specific fields and create test data.
Initially, I used to place a tests
folder within each Django app. However, I’ve since switched to having a single tests
folder at the root of the project, with subfolders for each app. This approach has made navigation much smoother and keeps everything organized. Plus, it makes sharing common fixtures much easier since they can be defined at a higher level and reused across multiple test modules.
tests/
├── accounts/
│ ├── __init__.py
│ ├── factories.py
│ ├── test_managers.py
│ ├── test_models.py
├── common/
│ ├── __init__.py
│ ├── test_validators.py
├── plaid/
│ ├── conftest.py
│ ├── dummy_data.py
│ ├── factories.py
│ ├── test_models.py
│ ├── test_services.py
│ ├── test_tasks.py
│ ├── test_views.py
│ ├── test_webhooks.py
├── __init__.py/
└── conftest.py
A sample test:
class TestCreatePlaidLinkToken:
LINK_TOKEN = "link_token"
class MockLinkTokenResponse:
def to_dict(self):
return {
"link_token": TestCreatePlaidLinkToken.LINK_TOKEN,
"expiration": "2020-03-27T12:56:34Z",
"request_id": "request_id",
}
@pytest.fixture
def setup_mocks(self, mocker):
return {
"mock_link_token": mocker.patch(
"django_finance.apps.plaid.views.plaid_config.client.link_token_create",
return_value=self.MockLinkTokenResponse(),
),
"mock_logger": mocker.patch("django_finance.apps.plaid.views.logger"),
}
@pytest.fixture
def create_link_token(self, client):
def _create_link_token(data=None):
return client.post(
reverse("create_link_token"),
json.dumps(data) if data else None,
content_type="application/json",
)
return _create_link_token
def test_create_plaid_link_token_success(self, login, setup_mocks, create_link_token):
login()
response = create_link_token()
assert response.status_code == 201
data = json.loads(response.content)
assert "link_token" in data
assert data["link_token"] == self.LINK_TOKEN
setup_mocks["mock_link_token"].assert_called_once()
The Frontend
This project combines the following frontend tools:
HTMX allows you to add dynamic interactions to your project without needing heavy JavaScript frameworks. You can organize your Django templates with small, reusable HTMX snippets. This allows you to update specific parts of the page based on user actions, without requiring a full page reload. This pattern works great with Django’s views because each piece of content can be treated as its own view and then injected into the DOM dynamically.
Alpine.js is another lightweight JavaScript framework used to add interactivity. It plays nicely with Django’s template structure and provides quick, declarative behavior for things like modals, dropdowns, or toggles. In combination with HTMX, Alpine can give you just enough JavaScript to enhance the UX without having to deal with something like React or Vue. For instance, you can use Alpine to manage state in the frontend, like opening and closing modals or handling client-side validation.
Tailwind CSS handles the styling in this project, which speeds up the development of clean and responsive UIs. This project also uses Flowbite for pre-built UI components like modals, tooltips, and buttons. Flowbite integrates well with Tailwind and helps you avoid reinventing the wheel for common UI elements.
Django Cotton allows you to create reusable Django templates. I discovered this awesome library halfway through the project while trying to make a modal reusable. Initially, I tried using Django’s include
template tag, but when passing lots of context data, it quickly became cluttered. Django-Cotton allows you to create highly reusable components that bind well with your HTMX and Tailwind in an HTML tag-like syntax. For example:
<c-modal modal_id="logout-modal" modal_title="Are you sure you want to logout?" action_url="{% url 'account_logout' %}" />
Two-factor Authentication with AllAuth
For implementing authentication in this project, I used the AllAuth library, which also provides support for multi-factor authentication (MFA). It offers an MFA module that integrates seamlessly with your existing user authentication setup. You can enable 2FA via an authenticator app, adding an extra layer of security. You can also easily customize the templates AllAuth provides to match the look and feel of your app.
Conclusion
Overall, this project is primarily educational and has just enough features to get you started with integrating Django, HTMX, Alpine.js, Tailwind, and Plaid.
That being said, beware that there might be a few bugs here and there, and there’s always room for improvement and further reuse of components. But overall, it’s a solid foundation for anyone looking to explore these technologies.
Checkout the project on GitHub
If you would like me to explore the topics mentioned here in more detail, let me know in the comments.
Happy coding! 🖤
Top comments (0)