The last web framework you'll ever need
By anders pearson •
After that he began waking up in the morning with a transparent jelly like a tadpole’s tail all over his mouth. This jelly was what the scientists call un-D.T., Undifferentiated Tissue, which can grow into any kind of flesh on the human body. He would tear it off his mouth and the pieces would stick to his hands like burning gasoline jelly and grow there, grow anywhere on him a glob of it fell.
- William S. Burroughs, Naked Lunch
It seems like the whole software engineering world has spent the last year talking about using AI for “Spec-Driven Development“, “Gas Town“, “Dark Software Factories“, “Phoenix Architecture“, etc. No one quite knows what the future looks like, but a lot of people believe that a lot of the software we create will be written by AI.
I thought “let’s just cut out the middle-man” though. Why have AI write software to do things like take an HTTP request and turn it into an HTML page response when we can just have the AI take each request and return the appropriate response directly? Why add all those extra steps of writing software, compiling, etc?
The result is UnDT, the Last Web Framework You’ll Ever Need. It does exactly that. You write your whole application as a markdown spec document and run uv run undt app.md and it starts up a web server. You make requests to it with your browser and it uses an LLM to generate a response for you.
All you need to write now is a markdown description of your application. Let’s start with the classic “Hello, World!” app:
# Hello World
Every page in this site is just the string "Hello, World!".
It should be plain text or simple HTML as appropriate.
Run uv run hello.md. Then you can make requests:
$ curl http://localhost:5000/
Hello, World!
$ curl http://localhost:5000/anything/at/all
Hello, World!
Let’s add localization. We just edit hello.md:
# Hello World
Every page in this site is just the string "Hello, World!" localized
to the language specified by the "Accept-Language" HTTP header
(defaulting to english).
It should be plain text or simple HTML as appropriate.
Then
$ curl -H "Accept-Language: sv-SE,sv;q=0.9" http://localhost:5000/
Hej, världen!
$ curl -H "Accept-Language: zh-CN,zh;q=0.9" http://localhost:5000/
你好,世界!
Cool. Now every web framework needs a TODO list application as a demo. So, here’s todo.md:
# TODO
This application is a simple single user TODO list.
## Conventions
- This app serves plain, dense HTML. No JavaScript framework. No CSS frameworks.
A little inline CSS in the `<head>` is fine and encouraged: serif body
text, generous margins, hairline rules between sections. JavaScript
for functionality can be inline or in a single `<script>` block in
the header.
## Paths
- `/` - the index page. Lists any TODO items that have been added. At
the bottom is a form to add a new TODO, it should just be a single
text field and an "Add" button. It POSTs to `/add`. Each item in the
list has a checkbox. Clicking the checkbox for at item makes a
`DELETE` request to `/item/:id` and reloads the index page.
- `/add` - accepts a POST request with a TODO item to add. The item
should be added to the list in memory (along with an auto generated
unique id), then the view should reply with a 302 redirect back to `/`
- `/item/:id` - a `GET` request should show the TODO item with that id
(or a 404 if none match) along with a button to delete it, and a
hypertext link back to `/`. A `DELETE` request to this endpoint
should remove the item with that id from memory (or 404 if none
match) and then respond with a 302 redirect back to the index.
Run uv run undt todo.md and pull it up in a browser:
I can add a couple items:
and cross them off the list
Simple as that.
We can make a fancier site too:
# The Undt Registry
A public catalogue of objects that do not exist.
Every entry in the Registry describes an artefact — a tool, a garment, an
instrument, a machine — that was never built, along with its provenance,
its measurements, and the names of the people who almost made it. The
Registry has been maintained, on and off, since 1888. Nobody remembers who
started it.
## Conventions
- The Registry serves plain, dense HTML. No JavaScript. No CSS frameworks.
A little inline CSS in the `<head>` is fine and encouraged: serif body
text, generous margins, hairline rules between sections.
- Every page carries a footer with a small line of navigation back to `/`
and to `/random`.
- Accession numbers look like `UND-1913-0447`: the prefix, the year an
entry was filed, and a four digit sequence.
- The tone is that of a careful institutional archivist who has stopped
distinguishing between the real and the catalogued. Dry, specific,
faintly sad. Never winking at the reader.
## The shape of the site
- `/` — the front desk. A short statement of purpose, a search form
(`GET /search?q=`), and eight or so recent accessions as links.
- `/object/<accession>` — a full record: title, accession number, date
filed, materials, dimensions, a physical description, a provenance note,
and cross-references to two or three other accession numbers.
- `/search?q=` — results for the query, however oblique. Never zero
results; the Registry always finds something adjacent.
- `/random` — a `302` redirect to some object record.
- `/index/<letter>` — objects filed under one letter.
- Anything else — invent it if it sounds like it belongs here, or return a
`404` written in the Registry's own voice.
## Continuity
Accession numbers are permanent. If a record has been served before, or
cross-referenced from another page, its details must not drift. Use the
memory field to hold on to anything a future request will need to agree
with.
## API
`/api/objects` and `/api/objects/<accession>` return JSON with
`Content-Type: application/json`. Same records, no prose framing.
Loading the index:
clicking one of the items:
You can even search the registry:
Your imagination is the limit for what you can build with this framework. All without writing a line of Javascript or Python or any of that nerdy stuff. If you encounter a bug, just add “don’t make any mistakes” to your spec.
Performance is blazing fast. Depending on the LLM provider you use, you should easily be able to achieve several requests per minute. If that’s not fast enough, try telling it to hurry and generate responses faster. If that doesn’t work, rewrite it in Rust.
If you have truly advanced use cases, you should be able to extend this framework to generate Python code and execute it on the fly. If you need help with that, I am available at a reasonable hourly consulting rate.