Blog
A web interface without a JavaScript project?
FastHTML in the Python stack
Building a web interface for a small internal tool has become surprisingly expensive. You create a second project, pick a frontend framework, set up a build step and maintain dependencies in a second language. For a dashboard that shows three numbers from a database, that is out of all proportion.
FastHTML by Answer.AI takes the other route: the interface is written in Python, HTML is produced by calling functions, and interactivity comes from HTMX. I used it properly for the first time for a tool of my own.
What it feels like
A complete application looks like this:
from fasthtml.common import *
app, rt = fast_app()
@rt
def index():
return Titled("Home", P("Hello world"))
serve()
That starts a server on port 5001. No project scaffold, no configuration file, no build.
HTML elements are Python functions: Div, H1, P, Button. Attributes become arguments. cls stands in for the reserved word class, and a _ in an attribute name turns into a -, so hx_get becomes hx-get.
The real appeal is somewhere else. HTMX is a small JavaScript library that lets an element call a server through an HTML attribute and swap part of the page with the answer. The server sends back HTML, not JSON. That removes the whole translation layer where you would otherwise serialise data, read it back in the browser and turn it into HTML all over again.
In FastHTML you hand the button the function that produces the fragment:
@rt
def step_detail(key: str):
return Div(..., id="detailbox")
Button("Details",
hx_get=step_detail.to(key="durable"),
hx_target="#detailbox",
hx_swap="outerHTML")
The .to() builds the path including parameters out of the function itself. You never type a route as a string, and renaming does not break anything silently.
Another handy detail: the same route serves both cases. On a normal request FastHTML wraps the return value into a full page, on an HTMX request only the fragment comes back. You write the view once.
Where FastHTML does not belong
This became the most important point while trying it out.
FastHTML is a server framework. A running app needs a running process, and every HTMX interaction is a call to the server. The tool choice follows almost by itself:
- A good fit when a Python backend genuinely earns its keep: querying a SQLite database or an API live, holding state between requests, server-side logic. Or when an existing terminal application should get a web counterpart on top of the same core.
- A bad fit for a diagram that goes into a slide deck as a screenshot, has to work offline, or gets passed around as a single file. There a server only adds a dependency without contributing anything. Expanding, toggling, tabs and other purely click-driven interactivity need no server at all.
The rule of thumb it boils down to:
A server call needed or a Python backend in play, then FastHTML. Otherwise a file.
And if it does have to be a file
The two go together. You can build a FastHTML app, run it comfortably in the browser while developing, and export one self-contained HTML file from it at the end. Double-clickable, offline, sendable by mail.
FastHTML has no built-in function for that, but the recipe is manageable:
- Render every fragment up front. What would otherwise arrive from the server on click is turned into HTML strings with
to_xml()at export time and placed into the page as JSON inside a<script>. - Swap HTMX for data attributes. An
hx-getbecomes adata-detail, and a single click handler swaps in the matching entry from the embedded store. No HTMX, no server, just a little JavaScript right there in the file. - Write the whole page, again through
to_xml(), with<!doctype html>in front. - Compress:
to_xml(..., indent=False)gives you the page without line breaks, and the embedded CSS collapses to one line with are.sub(r"\s+", " ", CSS).
For the file to really work offline, the app has to do without foreign sources from the start: CSS embedded rather than loaded over a network, no external typeface, the bundled default stylesheet switched off with pico=False. Stick to system fonts and every machine shows the same picture.
The switch for it fits on one line:
if "--export" in sys.argv:
build_static()
else:
serve()
I went through this with an interactive document built around a timeline, with clickable detail cards and a toggle. The result: one file without a single external reference. Whoever receives it needs neither Python nor a web server, only a browser. For handing something to people without a development environment, that is the decisive difference.
But watch out: because every detail card ends up pre-rendered inside the file, it grows with the amount of content. With a lot of material it would probably be cheaper to embed the data as JSON instead of finished HTML and render it in the browser.
Traps that cost time
Checked against fasthtml 0.14.13, as of September 2026. Four things you do not see coming the first time:
- The SVG shapes live elsewhere.
Line,Circleand friends are missing fromfasthtml.common, they come fromfasthtml.svg. TakeSvgfrom there as well, because only that version sets the SVG namespace. Miss the import and the server starts cleanly, then throws theNameErroron the first request to the page. Titled("")produces an empty heading.Titledrenders its title as an<h1>. If you design your own header, return a plainDivinstead and set the page title throughfast_app(title=...).- Restarting is not reloading.
serve()restarts the server on code changes by itself, but the browser only reloads the page automatically withfast_app(live=True). On top of that, an app I had started detached in the background did not pick up my edits at all. You then debug against old code and read line numbers that no longer match 😕 Only a clean restart helps. serve()listens on every network address, not just the local machine. If you do not want that, setserve(host="127.0.0.1")explicitly.
The first trap shows a pattern worth remembering:
Errors show up at request time, not at start-up.
A running server proves nothing. Call every route individually, the fragments included, because a green landing page does not mean every detail renders.
Where you can run it
Underneath FastHTML runs Uvicorn, an ordinary Python web server. So anything that can execute a Python process will do.
Answer.AI has put together example projects for nine platforms: Hugging Face Spaces, Railway, Replit, Vercel, Heroku, Fly.io, Coolify, Modal and a DigitalOcean server. The repository has not been updated since September 2024, though, so check the instructions against the current documentation of the platform in question.
For your own machine, Answer.AI recommends nginx or Caddy as the front server. Not Apache, because it does not support ASGI, the asynchronous Python server interface Uvicorn is built on.
The flip side belongs in the picture: a purely static web space cannot serve such an app. This site here, for example, consists of pre-built files, there is no Python process for a FastHTML app to hook into. If you have nowhere to keep a process running, you end up back at the single-file export - and find that for many cases it is entirely enough.
Sources (checked on 15.09.2026): FastHTML · deployment examples by Answer.AI · HTMX