On this page
HTTP serving
deno desktop ships in Deno v2.9.0 and is not in a stable release yet. To try
it now, run deno upgrade canary to install the
canary build. The command, configuration
keys, and TypeScript APIs may still change before the feature is stable.
A deno desktop app serves its UI over local HTTP and points the embedded
webview at it. This keeps the app structure identical to a normal Deno website.
Deno.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 Jump to heading
When the binary starts:
- The runtime picks an unused local port and sets the
DENO_SERVE_ADDRESSenvironment variable totcp:127.0.0.1:<port>. - Your code calls
Deno.serve(...). The serve API readsDENO_SERVE_ADDRESS(set by Deno 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 Deno HTTP server. There is no desktop-specific serving API.
Deno.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>`;
deno desktop main.ts
The default-export form works too:
export default {
fetch(req: Request): Response {
return new Response("Hello!");
},
};
Why local HTTP? Jump to heading
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
deno runanddeno 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.
- Frameworks run unchanged. Next.js, Astro, Fresh, and others already ship a
production HTTP server.
deno desktopruns that server and points the webview at it. See Frameworks.
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 Deno → webview communication where the overhead matters, use bindings, which bypass HTTP entirely and route through in-process channels.
Network exposure Jump to heading
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
Deno.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 deno desktop for that part of your stack. Use deno run with an
explicit address, or build a separate service.
Custom port behavior Jump to heading
You cannot override the port Deno.serve() binds to
inside deno 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 DENO_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 = Deno.env.get("DENO_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 Jump to heading
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 = Deno.env.get("DENO_SERVE_ADDRESS").split(":").pop();
const settings = new Deno.BrowserWindow();
settings.navigate(`http://127.0.0.1:${port}/settings`);