After an eight-year hiatus from programming, I’m back to developing personal projects, primarily backend heavy web applications.
The last project I worked on was a Django application with a Postgres database, Vue for the frontend, and hosted on Heroku. This had been my preferred stack roughly between 2010 and 2016, with various JS frameworks for the frontend. Now that I'm back to developing, I wanted to pick a modern stack that I can specialize in for the next 5+ years.
This post is a detailed account of my journey in selecting the ideal web stack for 2024. I’ll walk you through my thought process, small experiments with various technologies, and the final decisions that led to my chosen stack.
Requirements
Productivity is the main focus. I want less friction at every stage, from development to deployment and hosting.
With my limited time, minimal learning curve is a must.
The technologies should still be highly relevant in 2030.
Minimal Javascript. The best JS is the no JS.
No containers for development and deployment.
My development machine will not have Docker or Node/npm.
Backend
Go was the most exciting option that I wanted to dive into. I’ve never used a compiled and statically typed language for web applications, and both of these were appealing to me. The simplicity of the language, built-in HTTP libraries, single binary artifacts, better concurrency, ease of deployment were all great. Compilation was so fast that it didn’t feel like a friction at all. On top of this, VSCode Go plugin was also well-built, surfacing type-errors on the fly.
However, after building a test application with no framework, I found the constant boilerplate code required was too much compared to Python, despite the minimal learning curve. I often found myself comparing how simpler my code would look in Python. I also evaluated Go web frameworks but nothing was on par with Django. While speed wasn’t a hard requirement for me, productivity was, so I continued my research.
Typescript was next. Despite my lifelong aversion to server-side JavaScript, Deno and Cloudflare Workers intrigued me. Unfortunately, I didn’t enjoy working with TypeScript; it still felt too much like JavaScript (it is!). As much as I wanted to invest in Cloudflare’s web stack, it felt like they’ve coupled their services too tightly with JS. Seeing npm install wrangler to get started was enough for me to move on.
Elixir was another contender. Phoenix and LiveView’s ability to reduce client-side JavaScript was attractive, and I appreciated the opportunity to learn a functional language. However, Elixir’s limited community growth made it less appealing for long-term investment. I think Elixir looks amazing, one of the most innovative languages out there. I just wish it was adopted more widely.
I also briefly looked at server-side Swift, another language I liked, but also disappointingly, there was no traction.
Re-evaluating Python
The Pros of Python:
I’m most familiar with Python and have been keeping up with the language for ML applications at work.
It's becoming (incrementally) faster with each release, and major companies are investing heavily to improve its performance. Mojo is a superset of Python and if they can deliver what they promise, this might end up being a major tailwind for Python.
It's become the default language for AI and is not losing relevance anytime soon.
It has unmatched library support, especially for ML.
The Cons of Python:
Python’s concurrency model has always been confusing to me. Go’s goroutines clicked with my brain better.
Lack of (enforced) types.
Its package management, though improved recently thanks to Poetry, is still messy.
Deployment hasn’t improved over time.
Sticking with Python made sense. What about the framework?
Unlike Go, we do have to use a framework to build web applications with Python (That’s probably inaccurate but it would be stupid to do so). Before choosing the framework, the most important decision I had to make was deciding where to render the HTML. Server-side or client-side? That was an easy decision; I couldn't think of a reason to decouple backend from frontend and the only major reason I could think of was if I had to do mobile apps which could benefit from the same backend API endpoints. For my use cases, this wasn't important.
I started researching Python web frameworks. I always loved Flask for simple projects but I often ended up re-implementing Django features like authentication and the admin interface. Fastapi looked promising but as the name suggests it seemed like mostly for building APIs and not meant to be used with a templating engine and rendering HTML. It did have Jinja2 templating support but I was hesitant to use it for that purpose as it seemed like not the priority of the framework. Django also had better data modeling and built-in authentication both of which I love.
I also considered a serverless approach with AWS Lambda with Chalice framework but I didn't want to deal with the complexity of AWS outside the work (I work at Amazon).
Ultimately, I returned to Django. It had seen consistent updates over the past eight years, with incremental stability and quality of life improvements. I did hope to see better PWA capabilities and easier "islands of interactivity" integrations. Maybe soon?
Surprisingly, there were no new frameworks in the last eight years that were as productive as Django (outside the other old-timers like Ruby on Rails and Laravel). Phoenix's launch in 2014 was probably the last most interesting framework I could find.
Frontend
htmx! I had previously experimented with Intercooler, the predecessor of htmx, and was happy to see the project evolve and gain popularity. htmx integration with Django's template partials was a joy to work with.
For CSS, I chose Tailwind. While its initial impression of cryptic CSS classes in HTML can be off-putting, after some time, I found it increased my productivity once I became familiar with the common classes. I was glad to see that it has a CLI binary so I could avoid npm.
htmx is great for HTTP calls, but I still needed client side interactivity like dialogs, toggling elements, modals, ordering, animations etc. Alpine.js provided a minimalist approach that fit well with my overall design philosophy, allowing me to enhance user interfaces with minimal fuss and avoid complex JavaScript.
Database
I had previously used SQLite for development thanks to Django's default setup and found it problematic to have different databases in development vs. production. Migrating from SQLite to Postgres later in the development process was also often painful.
Despite the recent hype around SQLite, I don't see it as simpler than using a managed Postgres instance for most use cases. Litestream and Fly.io's managed LiteFS service still lack the desired developer experience.
Instead, I'm happy to see the attention Postgres is getting. Services like Supabase and Neon are doing impressive work in making Postgres easier to scale and manage. My ideal database would scale to zero and scale up with minimal intervention from me. I’d also like to simplify my setup by using Postgres for text search and a simple worker queue.
Hosting
Heroku is dead. I was a big fan of Heroku and used it heavily for personal and professional projects. It's not a viable option anymore and Salesforce seems to abandoned it. Individuals like me are not the profit centers for companies at that scale and I understand why it's been neglected for small scale users.
So I started researching the Heroku of 2024.
While I did manage my own servers at some point in the past, I never enjoyed it, and I was okay to pay the premium to offload those cycles to more capable hands. I should note that Coolify is an interesting project that I did consider using at a single large Hetzner instance but again, I quickly realized my limited time wasn't best spent there.
Fly.io looked like the most innovative company in this space and it met most of my requirements. I deployed a few applications there last year. The developer experience was no where near what Heroku was. Fly.io detects the framework and language you're using and generates the Dockerfile for you for deployment. This was a good and acceptable abstraction for me. My main issue with Fly.io was their instability and lacking communication. I've had so many instances where I couldn't deploy for hours while their status page showed fully green lights, or my instances were just unresponsive for some unknown reason and I didn't have any notification from the Fly.io. Another issue I have with Fly.io is that they are not fully aligned with their customers on their vision. They started the company with the main objective being edge servers, replicating your instance into multiple geographies with ease. They positioned themselves with this being their biggest differentiator, but I assume most of their users just want a new Heroku: extremely simple deployment with excellent DX, managed servers with easy horizontal and vertical scaling.
On the plus side, there are very capable people working at Fly.io that I respect too much, so I'm confident that in the long term they'll smooth out all the issues. They erred on the side of speed and innovation, but unfortunately, while you're hosting people's businesses, you just can't break things that often. Their pricing is great, though I suspect they'll increase them once their offerings become more mature. Overall, I'm excited for the future of Fly.io and will go back to testing them in the future.
I then tested Render. I deployed a simple Django application to Render in 2018, the year it was founded. Surprisingly, it's been impressively stable for me. Render doesn't expose the same level of low-level capability to users as Fly.io does, but for my use cases, the simplicity is fine, even preferable. Their dashboard is also well designed and had great usability. Render is more expensive than Fly.io but especially for personal projects, the difference is negligible.
Unlike Fly.io, Render offers managed Postgres, but I’m not sure if it provides an advantage over using Supabase or Neon, considering I can host the application in the same region as the Postgres database with those services. My assumption is that a dedicated database company should be able to manage your database better than a generic host, but this needs to be validated.
Another worthy mention is Railway. Since I was leaning towards using Render again, I kept my experiment with Railway to a small deployment of a simple Django application. The experience was very smooth and deployment went through on the first try. Their pricing did seem complex.
At the end, I went with Render.
Conclusion
After thoroughly exploring and testing various technologies, I returned to familiar ground with Django and Postgres, enhanced by modern tools like htmx, Tailwind, Alpine.js, and hosted on Render. This combination offers the stability, ease of use, and the productivity that I was seeking.
You’ve often heard the advice: stick with what you know best. While there are many new and exciting technologies in the web development landscape, leveraging familiar tools and supplementing them with carefully chosen modern solutions allows you to focus on building without unnecessary complexity.
A big shoutout to all the developers and maintainers of these languages and frameworks. Your hard work makes it possible for us to create and innovate every day. Thanks a lot!