Beet

2 min read Original article ↗
Beet

The Harvest #9 - Multi-Interface Applications

Last month Ibegan exploringan agnosticRequest/Responsepattern so I could use the same router library for both a cli and a server.

Request

typeservercli
Vec<String>pathpositional args
HashMap<String,Vec<String>>query paramsoptions/flags

This is howbeetstack.dev works currently, the cli that builds the server and static html uses the same router as the server itself. A single project often contains several applications and using the same primitives reduces complexity.

But what happens if you go the other way? A single application with multiple interfaces?

Prior Art - Content Negotiation

Ever heard ofcontent negotiation? I'm embarresed to say that I only discovered it last week. The original vision for the web was so cool! Servers that treat clients as individuals who maintain maximum control, deciding how they'd like their content delivered:

# browser wants html

GET /users/42
User-Agent: Mozilla/5.0
Accept: text/html

# api wants json

GET /users/42
User-Agent: curl/8.0
Accept: application/json

All kinds ofAccept-headers exist, even for spoken languages!

HTTP Content Negotiation Diagram

Content negotiation fell out of favor for avariety of reasons, but the rise of clankers seems to have driven a bit of a resurgence. Wordpressrecently announcedsupport for theAccept: text/markdownheader in their api docs:

curl -sH 'Accept: text/markdown' https://developer.wordpress.org/reference/functions/get_permalink

Beyond HTML

The above example demonstrates serving either the raw data (json) or a html view (html/md), but what if we took this idea a little further?

Seeing as terminals are so hot right now, maybe you'd like to build a TUI alongside your website. Or maybe you'd like to provide the view as a json tree structure for a game engine ui. Usually that means writing two entirely seperate frontends, but it sounds like the kind of thing a framework should be able to do.

fn render_page(request: Request, page: Page) -> Response{
	match request.header("User-Agent"){
		"Mozilla/5.0"     => into_html(page),
		"openclaw/2026.3" => into_markdown(page),
		"curl/8.0"        => into_ansi(page),
		"bevy/0.18"       => into_scene(page),
	}
}

Beyond HTTP

Seeing as the Request/Response types are already agnostic to the transport we can start getting weird, for instance instead of sending the payload as a response body treating the request as an instruction to update an in-process persistent interface.

Take ThePrimeagean's very coolssh terminal.shopidea. Currently visitingterminal.shopin the browser simply tells you to go to your terminal, but with this technique a single server could be used to ssh into as a TUI, and also servethat exact same applicationas a web app. Heck while we're at it why not also spin up a native app usingbevy_ui!

Write once, deploy everywhere, but like this time for realsies.