I'm kicking off a new series of posts where I break down the design and development of some web apps I’ve built recently. It’s a fun way for me to review my own code (and probably get roasted for it), reflect on the challenges I ran into, and hopefully get some feedback from more experienced devs.
Each app gave me a chance to try out new technologies, so every post in this series will also dive into the pros and cons of the frameworks and libraries I used, along with my personal experience working with them.
This is post #1—let’s get into it.
Betview is the main project I’ve been maintaining for the past six months or so.
Here’s the link:
If you're curious about the source code, you can find the repo on my GitHub:
https://github.com/nicoferreira90/bet_tracker_orange
In essence, Betview is a web app version of an elaborate bet-tracking Excel spreadsheet that pro gamblers used to rely on back in the day. I’m not sure if those spreadsheets are still in use (these days, I imagine most have migrated to Google Sheets or more specialized tools), but I used several versions of them about a decade ago. They were incredibly useful—but also a huge pain to deal with.
Customization was a nightmare. They’d often break if you changed even a single formula, or if you switched to a different version of Excel. The logic inside them was so convoluted that adding features or modifying anything would almost always lead to disaster. And don’t even get me started on trying to export the data, or opening them in open-source alternatives—good luck with that.
Still, despite their quirks, those spreadsheets handled a lot of essential calculations. They were powerful tools, especially at a time when sports betting wasn’t as mainstream as it is today. Fast forward to now: betting is way more popular (though I’m personally not a fan of the parlay craze for several reasons), and I figured a lot of people could benefit from a modern, customizable, user-friendly—and free—bet-tracking app.
So that’s why I built Betview. I also had a pretty clear idea of how the core of the app should work: the foundational model would be a Bet class to represent each individual bet, with all the necessary fields. Then, all of a user's bets would render in a Bet History table—each bet as a row—just like a spreadsheet, and just like SQL, which I already had a working knowledge of.
Overall Application Structure and Logic
The first thing I did was create a CustomUser class and set it up as the default user model. This is encouraged by the Django docs, and by now it’s something I do by default whenever I start a new project. For authentication, I mostly replaced Django’s built-in system with a widely used third-party package called allauth, which offers a slightly more modern approach to authentication.
Next, I wrote the Bet model, which includes a ForeignKey field to associate each bet with the user who created it. I seeded the database with a bunch of test bets under my superuser account, and built the app’s main interface: the Bet History page. The view (Django’s equivalent to the controller in MVC frameworks) is a generic class-based ListView. The template starts with a table header and then loops through all bets associated with the active user, rendering one row per bet. By default, pagination is set to 20 bets per page, though I later added a user-configurable pagination option.
To manage user interaction, I added a class-based CreateView for adding new bets, an UpdateView for editing them, and a function-based view for deleting existing bets.
All logic related to the Bet model lives within a Django app called "bets". In Django, apps are modular components that handle a specific domain or function within the project. The bets app handles nearly everything related to the user's betting history: models, views, templates, and URLs.
The second app I created was "tags", which handles everything related to tagging bets: models, views, URLs, and templates. In Betview, a Tag object functions like a label that can be attached to a Bet object. This is implemented as a many-to-many relationship, meaning:
The main purpose of the Tag model is to support more flexible filtering and analysis. Tags offer an additional dimension for organizing and segmenting data in the Analytics panel. For example, you could filter for only those bets you’ve labeled "PGA", "Manchester United", "Away Teams", or "Pre-season Games", in addition to more rigid filters like date, sportsbook, or bet type—which are hardcoded into the Bet model.
The tags app includes views, URLs, and templates for creating, updating, and deleting tags, as well as a dedicated Bet-Tag page to manage tag associations for individual bets. In hindsight, I think the idea of tagging bets was a solid design choice. However, though Betview has a fair number of active users, I believe the feature is underused.
The third and final main app in Betview is called "analytics", which handles everything related to the Analytics Panel—views, templates, and URL routing. This app doesn't contain any models. It was by far the most complex part of the project to get working the way I envisioned (excluding CSS of course!).
The analytics panel computes various bet payouts and aggregated metrics like ROI and profit/loss, and features an interactive graph powered by the Plotly API. The graph and results update dynamically every time the user hits the FILTER button. Behind the scenes, this is done by chaining Django ORM .filter() calls as needed based on the selected criteria.
Th view logic got pretty gnarly at times, and I’m sure I made a few suboptimal design decisions due to inexperience. Still, I managed to get it working reliably.
(By the way, I took inspiration for the layout and general feel of the analytics page from a web app I’ve often used for NFL handicapping: https://rbsdm.com/stats/stats/ The similarities will be obvious. As far as I could tell, no source code was available, so I had to reverse-engineer the interface from scratch.)
During development, I started out using Django’s default SQLite database. Later, I migrated to PostgreSQL for both development and production environments. I used Psycopg as the database adapter. I’ve worked with PostgreSQL even before I picked up Python, so it’s been great that Django supports it as a first-class database.
Difficulties I Encountered During Development
I’ve written before about my struggles with making CSS behave the way I want—even when using frameworks like Bootstrap or Tailwind—and this project was no exception. I felt like I spent way more time than I should have just trying to get things to look right.
I used Bootstrap extensively, and it definitely helped—especially for styling buttons, tables, and forms. (Shoutout to Django Crispy Forms, which is an absolute lifesaver when dealing with form styling.) But I still ran into issues with positioning elements exactly as intended and dealing with implicit styles baked into Bootstrap components. Editing something as seemingly simple as the Navbar’s dropdown menu took way longer than it should have. As always, I resorted to using !important (yeah, I know—bad practice), mostly because I didn’t want to dive into the weeds of modifying Bootstrap’s base files.
Another area I struggled with was mobile responsiveness. From the start, I envisioned Betview as a desktop-first experience—a full statistical interface meant for use on a laptop or monitor-sized screen. I don’t think I’ve managed to make the site fully mobile-responsive, and honestly, I think the only proper way to do it would be to create alternative, more minimal renderings of the tables and panels specifically for small screens. I might do that someday, but right now I just don’t have the time for a full UI rework.
Besides, front-end design has never been my favorite part of development, so mastering mobile responsiveness isn’t exactly high on my list of priorities. As it stands, I think the site looks pretty good on larger screens, and just okay on mobile. I do test each view on my phone with every update, though, just to make sure nothing breaks.
There was one thing I never managed to get working, and it’s the only feature I truly gave up on: I used Plotly’s API to build interactive results graphs, and throughout the project I relied on HTMX to dynamically re-render parts of the DOM. HTMX worked beautifully, and it was super satisfying to have large portions of the app behave like a single-page application.
But what I really wanted was for both the results graph and the table of statistics on the right side of the Analytics Panel to update dynamically when the user clicked the FILTER button—without having to reload the entire page. Something about the interaction between Django, HTMX, and Plotly just didn’t play nice. When I used HTMX to replace the HTML partial, the graphs would be all janky.
I really wanted to make it work. At one point, I spent like four hours rewriting the entire page from scratch—but the new version had graphs that were just as broken, if not worse. I could never figure out what was causing the issue. ChatGPT and GitHub Copilot didn’t help much, and although I asked around on Reddit and Discord, I didn’t get any answers that solved the problem.
In the end, I decided it was a relatively small compromise and left it the way it originally worked: the entire Analytics page reloads when the filter form is submitted. Not ideal, but at least it’s consistent and reliable.
Interesting Tools I Used on This Project
As I mentioned, I used HTMX extensively for making the Django templates more dynamic. What HTMX does is basically to allow the backend to dynamically swap parts of the DOM in response to user interactions—like button clicks or form submissions—without requiring a full page reload. It enables this behavior using simple HTML attributes, making it easy to build modern, interactive web interfaces without writing custom JavaScript. I tried to use HTML partials as much as possible, and took advantage of HTMX’s hx-get, hx-put and hx-delete to make large swaths of Betview feel much like a SPA.
To a lesser extent, I also used Alpine.js, another lightweight JavaScript library. Alpine.js is much more front-end-focused than HTMX, and I used it to handle tasks like toggling options on and off in the analytics form, as well as implementing a dynamic search function for the rows in the Bet History page.
I find both HTMX and Alpine.js extremely useful, and HTMX especially so. I find the whole idea of extending HTML for AJAX-purposes intriguing, as it very much runs counter to the way front-end development is currently done (i.e. developing SPA UIs that get fed JSON from a web API).
I developed the entire application within a Docker container, which meant I didn’t use the traditional Python venv module to create a virtual environment. For those unfamiliar, Docker is a platform for building, running, and managing applications inside lightweight, portable containers. I used Docker alongside Windows Subsystem for Linux (WSL) to create a small Linux environment within my Windows machine, where I developed Betview. I think that setup is pretty neat!
Finally, Betview is deployed on Heroku (unlike this blog, which is hosted on Fly.io). A lot of people don't like Heroku, but I’ve found its Django + Docker support to be solid. It’s generally easier to use, and much more stable and reliable, than Fly.io (though Heroku no longer has a free tier), and with the level of daily traffic Betview typically gets, it would likely exceed Fly.io’s free tier anyway. I find deploying applications via the Git command line helpful, and setting up add-ons on Heroku feels more intuitive than it does on Fly.io.
Final Words
I think this post has gone on long enough, so I’ll wrap it up here. Thanks for reading, and stay tuned for a similar breakdown of this very site, Django Horizons, which uses a different set of tools from Betview, including Wagtail, Tailwind CSS, and Fly.io.
© 2025 Nicolás Ferreira. All rights reserved. Landscape Vectors by Vecteezy.