HTTP serving
A ende desktop app serves its UI over local HTTP and points the embedded
webview at it. This keeps the app structure identical to a normal Ende website.
Ende.serve() is the entry point and every request
flows through your handler, but with no port to manage and no remote network
exposure.
How it works
When the binary starts:
- The runtime picks an unused local port and sets the
ENDE_SERVE_ADDRESSenvironment variable totcp:127.0.0.1:<port>. - Your code calls
Ende.serve(...). The serve API readsENDE_SERVE_ADDRESS(set by Ende itself in this mode, not by the user) and binds to that port, ignoring whatever port you pass. - The webview navigates to
http://127.0.0.1:<port>once the listener is ready.
You write the same handler you would for any Ende HTTP server. There is no desktop-specific serving API.
Ende.serve((req) => {
const url = new URL(req.url);
if (url.pathname === "/api/hello") {
return Response.json({ hello: "world" });
}
return new Response(HOMEPAGE, {
headers: { "content-type": "text/html" }
});
});
const HOMEPAGE = `<!doctype html>
<html><body>
<h1>Hello, desktop</h1>
<button onclick="fetch('/api/hello').then(r => r.json()).then(console.log)">
Ping
</button>
</body></html>`;ende desktop main.ts
The default-export form works too:
export default {
fetch(req: Request): Response {
return new Response("Hello!");
}
};Why local HTTP?
The local-HTTP architecture trades a tiny amount of overhead for properties that matter for desktop apps:
- Same code in browser and desktop. The homepage, fetch, websockets, and
cookies all behave identically in
ende runandende desktop. You can develop in a browser tab and ship the same code as a desktop binary. - No special module system. Imports, static assets, and module-level code all run the way they would for a web server.
The cost is a single network hop within 127.0.0.1 per request. For UI serving
(HTML, CSS, bundled JS, JSON API responses) this is negligible.
For high-throughput Ende → webview communication where the overhead matters, use bindings, which bypass HTTP entirely and route through in-process channels.
Network exposure
The bound address is always 127.0.0.1 (or [::1]). The compiled binary
never binds to a public interface, even if you pass 0.0.0.0 to
Ende.serve(). Other apps and other users on the
same machine cannot reach your server.
If you need to serve users on other machines (a self-hosted local server), do
not use ende desktop for that part of your stack. Use ende run with an
explicit address, or build a separate service.
Custom port behavior
You cannot override the port Ende.serve() binds to
inside ende desktop. This is intentional: the webview needs to navigate to the
same port the runtime is listening on, and the runtime is the source of truth
for that value.
If you need to know where the server is bound, read ENDE_SERVE_ADDRESS. It is
in tcp:127.0.0.1:<port> form, so split off the port when you need an http://
URL:
const addr = Ende.env.get("ENDE_SERVE_ADDRESS")!; // "tcp:127.0.0.1:54321"
const port = addr.split(":").pop();
console.log("Serving on:", `http://127.0.0.1:${port}`);
Serving multiple windows
When you create additional windows, they all load from the same local HTTP server by default. Use different paths per window to differentiate:
const port = Ende.env.get("ENDE_SERVE_ADDRESS")!.split(":").pop();
const settings = new Ende.BrowserWindow();
settings.navigate(`http://127.0.0.1:${port}/settings`);