← Back to blog

How to expose an HTML interface in a macOS app

FlyingFox + swift-html: how to embed an HTTP server in a macOS app to serve web interfaces from the Bundle, without heavy frameworks.

While developing a macOS application, I found myself needing to display custom web interfaces from inside the app itself, without having to build the UI in AppKit or SwiftUI. In what I’m working on, there are times when you can’t access the app’s main interface directly, and with remote access we reach this lightweight interface to execute changes or apply configurations to the main app.

I was looking for something simple, fast, that could load local resources (HTML, CSS, JS) stored in the project’s Bundle and that didn’t depend on heavy or outdated frameworks like GCDWebServer.

Searching for modern alternatives, I found two tools that together solved everything:

  • FlyingFox: a micro HTTP server written in Swift Concurrency, ideal for embedded apps
  • swift-html: a 100% Swift HTML DSL, created by the Point-Free authors

This post explains how to integrate them, how to correctly serve static content from the app bundle, and how to build the UI directly from Swift.

Why not use SwiftUI?

Many times I want to display an interface that isn’t necessarily through the app’s main window, and doing it in HTML gives me flexibility. Also, if I work alone (as in my case) and I want a quick way to create a UI without depending on external design or visual tools, the Swift DSL lets me do everything from code.

I’m also interested in having this interface be remotely accessible, or simulating a web frontend without spinning up a real server.

Setting up the server with FlyingFox

import FlyingFox

let server = HTTPServer(port: 8002)

try await server.appendRoute("GET /static/*", to: .directory(
  subPath: "",
  serverPath: "static"
))

This mounts the static/ folder as the public base, and any file inside the Bundle will be accessible from /static/.... You can restrict access as much as needed to avoid unnecessarily opening the flood gates. This is important: if you use Xcode groups, they’ll have no effect — you need actual physical folders in the project.

Generating HTML using pure Swift (DSL)

import Html

let document: Node = .document(
  .html(
    .body(
      .h1("Welcome"),
      .p("This is an embedded interface using HTML DSL in Swift."),
      .img(src: "/static/images/background.jpg", alt: "Background image"),
      .script(src: "/static/js/app.js"),
      .link([.rel(.stylesheet), .href("/static/css/style.css")])
    )
  )
)

let htmlString = render(document)
return HTTPResponse(html: htmlString)

With this, I no longer need .html files, and everything stays inside the Swift project. Ideal when working without a frontend team.

What happens if a resource doesn’t load?

This cost me a few minutes to figure out: the bundle doesn’t preserve virtual folders like www/ or public/. All files are placed directly in the bundle’s base folder. So serverPath: "" (empty like that) works, but trying to use “folder” as part of the path doesn’t — unless you have a physically created folder in the project.

Result

With this setup I can serve HTML generated from code, access my CSS, JS and image resources and display it in any WebView or browser without depending on any other heavy library. I can also modify everything from Swift code without opening HTML editors.

Another huge advantage: the compiler warns me if I’m writing something wrong, since the DSL library is typed and works very similarly to a functional language like SwiftUI.


What if I need to use HTML templates?

I could add them as .html or .stencil files and load them from the bundle. FlyingFox has no problem with that, but since in my case I prefer to keep everything embedded in Swift (for efficiency and portability), using the DSL was most practical.


Conclusion

Sometimes it’s necessary to look for solutions outside the app’s main context. The notion that every interface must be built on a window, with views in SwiftUI or UIKit, doesn’t always give us the ability to solve all needs. Occasionally we need to reach for these kinds of solutions — exposing HTML content directly in the browser to expand our options and ultimately make users happy.