16 min read

Choosing a Web Framework for RST Platform

Table of Contents

I have a fairly clear hypothesis for the overall architecture of the RST Platform. The platform will initially run on a single VM, with systemd responsible for process lifecycle and orchestration. The individual components will be relatively small, standalone programs with specific responsibilities, and Python will be the default language across the platform.

One of those programs needs to provide the human interface. This is where the choice becomes less obvious, because selecting Python as the platform language does not automatically tell me what kind of web application I should build, or even whether Python should remain the right language for the human interface in the long term.

Before choosing a framework, I therefore want to make the requirements for this particular part of the architecture explicit.

Choosing a Web Framework for RST Platform

The Human Interface

The current hypothesis is that the human interface should be a server-rendered web application rather than a single-page application backed by an API. The interface will contain features for configuring the platform, viewing dashboards and graphs, reviewing screenings and results, and interacting with the workflows that the other programs provide.

HTML should primarily be rendered on the server. HTMX will provide server-driven interactivity by making HTTP requests and replacing relevant parts of the page with HTML returned by the server. Alpine.js will complement this by handling the relatively small amount of browser-local state and behavior where communicating with the server would be unnecessary.

The architecture can therefore be summarized as:

                              Browser
                                 β”‚
                         β”Œβ”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”
                         β”‚               β”‚
                        HTMX         Alpine.js
                       server         browser
                     interaction     local state
                         β”‚               β”‚
                         β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
                                 β”‚
                              HTML/HTTP
                                 β”‚
                                 β–Ό
                       Python web application
                                 β”‚
                   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                   β”‚             β”‚             β”‚
                   β–Ό             β–Ό             β–Ό
            Authentication  Authorization  Validation
                   β”‚             β”‚             β”‚
                   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                 β”‚
                                 β–Ό
                        Application logic
                                 β”‚
                                 β–Ό
                          Platform data
                       DuckDB / JSON / YAML

There are some additional requirements that matter for the framework decision. The interface will require authentication, and I expect a relatively small number of roles with different authorizations. Some users may primarily consume information while others can modify configuration or initiate operations. The application will also contain forms and other user input that need proper server-side validation, including returning useful validation errors as HTML.

The application needs to interact directly with DuckDB as well as JSON and YAML files. There may eventually be some HTTP APIs, but I currently see those as a secondary concern rather than the primary interface to the application.

Together, these assumptions describe a fairly traditional server-rendered web application, but with HTMX providing a more interactive experience than the traditional full-page request-response cycle.

They also provide much more useful selection criteria than simply asking which Python web framework is best.

Keeping HTMX and Alpine.js Independent

One requirement deserves particular attention because it affects how I evaluate the frameworks: I do not want the server framework to own the relationship with HTMX.

HTMX is part of the presentation architecture, but that does not mean it should become part of the Python application architecture. HTMX communicates with the server through HTTP. It sends ordinary requests with some additional HTTP headers and receives HTML and HTTP response headers in return.

I would therefore prefer the relationship to remain direct, rather than introducing another abstraction between HTMX and the framework:

     Preferred                          Avoided

        HTMX                             HTMX
         β”‚                                 β”‚
        HTTP           framework-specific HTMX integration
         β”‚                                 β”‚
Python web framework                Python web framework

The difference may initially seem insignificant, but it has consequences for independent evolution. If the Python framework has dedicated HTMX request types, response objects, helpers, or plugins, I introduce another version relationship. A new HTMX capability can potentially depend on the framework integration being updated to understand it.

There is little reason to introduce that dependency when the underlying protocol is already HTTP. If HTMX sends a header, the Python application can read the header. If a response needs an HTMX-specific header, the application can set one.

Alpine.js should be even more independent. It deals with browser-local behavior, so there should generally be no reason for the server to know that Alpine.js exists at all.

This separation gives me an architectural property that I value more than convenient integration: the browser libraries and server framework can evolve independently.

With that requirement established, I can evaluate the actual framework choices.

Django

Django is probably the strongest candidate if I look only at the requirements of the human interface. It has mature and integrated solutions for authentication, authorization, sessions, forms, validation, templates, middleware, and application organization. Users, groups, and permissions are established framework concepts, and its forms model is specifically designed for the kind of server-rendered form workflow that this application will need.

That makes Django particularly attractive for the parts of the requirements that are easiest to underestimate. Authentication is not merely a session primitive that I need to turn into an authentication system. Authorization does not have to start with designing my own representation of users, roles, and permissions. Form parsing, validation, error handling, and rendering are all part of the same application model.

HTMX also fits Django surprisingly naturally. There is nothing about HTMX that requires an API framework. A Django view can receive a request and return either a complete HTML document or a fragment of HTML, which is exactly what HTMX needs.

The concern with Django is therefore not whether it can implement RST Platform. It clearly can. The concern is how much additional architecture I adopt by choosing it.

Django has an extensive application model built around concepts such as Django applications, models, migrations, and its ORM. These are significant advantages when the web application itself owns the application’s persistence model. RST Platform is different. DuckDB and files already exist as parts of the broader platform architecture, and the web application should interact with them rather than redefine persistence around the needs of the web framework.

It is possible to use Django without making its ORM central to everything, but having to explain which substantial parts of the framework architecture I deliberately do not want is a warning sign. Django may simply provide more application framework than this particular program requires.

Its strongest argument remains authentication, authorization, and forms. If those areas become a substantial part of the complexity of the human interface, the additional weight of Django could become justified. For the current requirements, however, I suspect it is more than I need.

FastAPI

FastAPI presents almost the opposite tradeoff. It is an excellent framework for building typed HTTP APIs, with strong request validation, dependency injection, serialization, automatic OpenAPI generation, and security mechanisms that fit API-oriented systems particularly well.

FastAPI can also render Jinja templates, process HTML forms, maintain sessions through the underlying Starlette functionality, and return arbitrary HTML responses. There is no technical reason it could not be used for RST Platform.

The question is whether doing so would mean selecting a framework for capabilities that are not central to the application.

The dominant interaction in RST Platform is likely to look something like an HTML form submitted as an HTTP request, validated, run against an application operation, and answered with an HTML fragment rather than a JSON payload.

FastAPI’s natural center of gravity is closer to a typed request producing a structured API response. Its strengths around JSON models and OpenAPI become much less important when most requests ultimately produce HTML.

The same issue appears with authentication. FastAPI has strong support for API-oriented security mechanisms, but RST Platform is primarily a browser application where conventional session-based authentication is a natural fit. Again, FastAPI can support this, but it is not the problem around which the framework has primarily been designed.

If RST Platform eventually develops a significant external API that becomes a stable platform contract, this assessment could change substantially. Under the current assumptions, however, FastAPI seems to optimize the secondary interface rather than the primary one.

Flask

Flask approaches the problem from the other direction. Rather than providing an integrated application architecture, it provides a relatively small set of web abstractions and leaves most of the application architecture to the application itself.

That initially sounds like a weakness. For RST Platform, I think it may be the primary advantage.

Flask provides routing, requests and responses, sessions, Jinja templating, static resources, middleware hooks, and mechanisms such as Blueprints for organizing a larger application. It does not require an ORM, does not dictate the persistence model, and does not need to understand how the rest of RST Platform works.

Most importantly, Flask does not need to know about HTMX.

From Flask’s perspective, an HTMX request is simply an HTTP request. The application can inspect an HTTP header if it needs to distinguish an HTMX request from a conventional request. Returning an HTML fragment is simply returning HTML. Sending an HTMX response header is simply setting an HTTP header.

Alpine.js can remain completely invisible to Flask.

This produces a pleasingly simple dependency structure: HTMX and Flask each depend only on HTTP, not on each other. Alpine.js remains entirely in the browser, invisible to either side of that boundary.

The important benefit is not merely that Flask has fewer features. It is that the technologies meet at relatively durable boundaries. HTMX can be upgraded without waiting for Flask. Flask can be upgraded without caring which HTMX version the browser uses. Alpine.js can change without affecting the Python application at all.

Flask’s weakness becomes clearer when considering authentication, authorization, and validation. Django provides considerably more in these areas. Flask provides the web primitives, including sessions, but more decisions need to be made about how the actual authentication system, roles and permissions, form validation, and related concerns should work.

That is real additional work and should not be dismissed merely in the name of minimalism. The question is whether these requirements are sufficiently complex to justify adopting a larger application framework.

My current assumption is that they are not. I expect a relatively small number of roles and a manageable set of forms rather than a large enterprise identity and workflow application. Under those conditions, having to make explicit choices around these areas may be a reasonable price for keeping the framework itself small and loosely coupled to the rest of the architecture.

Litestar

Litestar is perhaps the most interesting alternative to Flask because it sits somewhere between the minimalism of Flask and the more structured approach of frameworks such as FastAPI and Django.

It provides strong validation, dependency injection, authentication and authorization mechanisms, templating, and modern Python typing. Its guards provide an elegant mechanism for protecting routes and operations, which maps well to the expectation that RST Platform will have a relatively small number of roles with different permissions.

Litestar also explicitly supports HTMX. On a feature checklist, that might appear to make it the obvious candidate for this architecture. I actually see that integration as something I would prefer not to use.

The problem is not Litestar’s implementation. It is the dependency relationship it creates. If HTMX can already communicate with any HTTP server, having HTMX-specific request and response abstractions inside the server framework provides relatively little architectural value. It instead couples the server application to the framework’s understanding of HTMX and potentially to the versions and capabilities that integration supports.

Litestar does not require its HTMX integration, so this is not a reason to reject the framework. I could simply use Litestar as an HTTP framework and let HTMX communicate with it through ordinary HTTP. Doing so, however, removes one of the apparent differentiators in Litestar’s favor.

What remains is still compelling: stronger built-in abstractions for validation, authentication, authorization, dependency injection, and application structure than Flask provides, without adopting the full Django application model. The question is whether RST Platform needs those abstractions badly enough to make them part of the framework dependency.

At the moment, I am not convinced that it does.

Starlette and FastHTML

There are other alternatives, but I currently see less reason to pursue them.

Starlette could provide an even thinner foundation than Flask, particularly if I wanted an ASGI-based application. The problem is that minimalism eventually reaches a point of diminishing returns. Once authentication, authorization, templates, forms, validation, and application organization are considered, I would be assembling more of the application infrastructure myself without gaining much additional architectural independence. The goal is not to choose the framework with the fewest features. It is to choose the smallest abstraction that comfortably solves the problem.

FastHTML is interesting for almost the opposite reason. Its close relationship with HTMX aligns well with the interaction model I want, but that same relationship makes it less attractive as the durable server-side foundation. I want HTMX to be an important part of the presentation architecture without making the Python application itself HTMX-specific. Conventional HTML templates and HTTP provide a cleaner boundary for that.

Neither option currently gives me a compelling reason to move away from the main candidates.

Comparing the Frameworks

The comparison becomes more useful when evaluated against the actual architecture rather than generic framework capabilities. Starlette and FastHTML are left out of the table below since neither made it past the initial screening above. Ratings run from β˜…β˜†β˜†β˜†β˜† (poor) through β˜…β˜…β˜†β˜†β˜† (fair) to β˜…β˜…β˜…β˜…β˜… (excellent).

Requirement Django FastAPI Flask Litestar
Server-rendered HTML β˜…β˜…β˜…β˜…β˜… β˜…β˜…β˜…β˜†β˜† β˜…β˜…β˜…β˜…β˜… β˜…β˜…β˜…β˜…β˜†
Partial HTML for HTMX β˜…β˜…β˜…β˜…β˜… β˜…β˜…β˜…β˜…β˜† β˜…β˜…β˜…β˜…β˜… β˜…β˜…β˜…β˜…β˜…
HTMX without framework coupling β˜…β˜…β˜…β˜…β˜… β˜…β˜…β˜…β˜…β˜… β˜…β˜…β˜…β˜…β˜… β˜…β˜…β˜…β˜…β˜…β€ 
Alpine.js independence β˜…β˜…β˜…β˜…β˜… β˜…β˜…β˜…β˜…β˜… β˜…β˜…β˜…β˜…β˜… β˜…β˜…β˜…β˜…β˜…
Session-based authentication β˜…β˜…β˜…β˜…β˜… β˜…β˜…β˜…β˜†β˜† β˜…β˜…β˜…β˜†β˜† β˜…β˜…β˜…β˜…β˜†
Route protection β˜…β˜…β˜…β˜…β˜… β˜…β˜…β˜…β˜…β˜… β˜…β˜…β˜…β˜†β˜† β˜…β˜…β˜…β˜…β˜…
Role/permission authorization β˜…β˜…β˜…β˜…β˜… β˜…β˜…β˜…β˜…β˜† β˜…β˜…β˜…β˜†β˜† β˜…β˜…β˜…β˜…β˜…
HTML form handling β˜…β˜…β˜…β˜…β˜… β˜…β˜…β˜…β˜†β˜† β˜…β˜…β˜…β˜†β˜† β˜…β˜…β˜…β˜…β˜†
Form validation ergonomics β˜…β˜…β˜…β˜…β˜… β˜…β˜…β˜…β˜…β˜† β˜…β˜…β˜…β˜†β˜† β˜…β˜…β˜…β˜…β˜…
Validation errors rendered into HTML β˜…β˜…β˜…β˜…β˜… β˜…β˜…β˜…β˜†β˜† β˜…β˜…β˜…β˜†β˜† β˜…β˜…β˜…β˜…β˜†
Small API surface β˜…β˜…β˜…β˜†β˜† β˜…β˜…β˜…β˜…β˜† β˜…β˜…β˜…β˜…β˜… β˜…β˜…β˜…β˜…β˜†
No ORM assumption β˜…β˜†β˜†β˜†β˜† β˜…β˜…β˜…β˜…β˜… β˜…β˜…β˜…β˜…β˜… β˜…β˜…β˜…β˜…β˜…
DuckDB and file-oriented architecture β˜…β˜…β˜…β˜†β˜† β˜…β˜…β˜…β˜…β˜… β˜…β˜…β˜…β˜…β˜… β˜…β˜…β˜…β˜…β˜…
Framework minimalism β˜…β˜…β˜†β˜†β˜† β˜…β˜…β˜…β˜†β˜† β˜…β˜…β˜…β˜…β˜… β˜…β˜…β˜…β˜†β˜†
Low architectural coupling β˜…β˜†β˜†β˜†β˜† β˜…β˜…β˜…β˜…β˜† β˜…β˜…β˜…β˜…β˜… β˜…β˜…β˜…β˜…β˜†

† Assuming Litestar’s dedicated HTMX integration is not used.

Two of the rows deserve a note. Framework minimalism measures how much surface area the framework itself has; low architectural coupling measures how much it dictates the surrounding application’s structure once adopted. Django scores worse on the latter than the former for exactly that reason: it is not just a large framework, it is one that wants to own the persistence model.

What I find interesting about this comparison is that there is no meaningful capability problem. All four frameworks can implement the application I have described. The differences are primarily about where each framework places its abstractions and how much of its preferred application architecture I would adopt along with it.

Django gives me the most complete server-rendered application framework, but also asks me to accept the most surrounding structure. FastAPI provides excellent API abstractions that are currently peripheral to the main problem. Litestar occupies an attractive middle ground but introduces more framework concepts than I am currently convinced I need. Flask provides less, but what it provides maps closely to the boundary I actually need from the web framework.

The Current Hypothesis: Flask

My current hypothesis is therefore that Flask is the best starting point for the RST Platform human interface.

This is not because Flask wins every individual comparison. It clearly does not. Django has a stronger integrated model for authentication, authorization, forms, and validation. Litestar provides more sophisticated built-in application abstractions. FastAPI would be a stronger choice if APIs were the primary purpose of the application.

Flask instead appears to provide approximately the amount of web framework that RST Platform currently needs. The surrounding architecture already exists independently of the web application, so I do not need the framework to become the architecture. I need it to provide a clean HTTP and HTML boundary between the browser and Python.

This hypothesis depends on some assumptions. Authentication needs to remain relatively straightforward. Authorization needs to remain manageable with a small number of roles or permissions. Forms and input validation need to remain significant requirements without becoming the dominant complexity of the application.

As long as the human interface remains primarily server rendered, with HTMX providing server interaction, Alpine.js handling limited browser-local state, and authentication, authorization, and forms remaining reasonably straightforward, Flask appears to be the simplest Python framework that fits the architecture.

If the Assumptions Change

If those assumptions prove wrong, the framework decision should be reconsidered rather than defended. If authorization becomes substantially more sophisticated or the interface becomes dominated by complex data-entry workflows, Django may justify its larger application model. If stronger typed validation and application-level security abstractions become important without needing the rest of Django, Litestar may become the better balance. If APIs become a significant external platform contract, FastAPI or Litestar would deserve another look.

There is also a more fundamental possibility than replacing one Python framework with another: Python itself may eventually stop being the right choice for the human interface. If the interface evolves into a rich application with significant client-side state or complex interactions, the useful question may no longer be which Python framework should replace Flask, but whether the boundary of the human interface has changed enough to deserve a different architecture altogether β€” building it separately, in something such as SvelteKit, rather than stretching a server-rendered Python framework to fit.

For now, though, Flask keeps the human interface consistent with the rest of RST Platform: a small, standalone Python program with one clear responsibility, running like any other service under systemd.


πŸ’¬

Building a similar server-rendered, HTMX-driven interface and landed on a different framework? I would love to compare notes. Leave a comment below, or reach out through another channel if you would rather keep the conversation private. Follow the RSS feed for more.