Muzak logomuzak
v0.2.00
You are reading the 0.1.1 documentation. The current release is 0.2.0. Read this page for it →

Static Files and Frontends

Two mounts, one machinery. Router.Frontend serves the output of a frontend build and resolves a path with no file behind it; Router.Static serves a directory and lets a miss stay a miss.

// Assets that belong to no particular route. A static mount serves what it
// finds and nothing else, so a miss here stays a miss rather than being
// answered with the application document by the frontend below.
app.Static("/static", muzak.StaticOptions{Dir: "static"})

// The built frontend is served last: every route above is matched first, so
// mounting at the root cannot shadow the API.
app.Frontend("/", muzak.FrontendOptions{Dir: "dist"})

Nothing is rendered on the server and nothing is built here. Both serve files that already exist, which is what npm run build and its equivalents produce.

Routes win

A request is matched against every registered route first and reaches a mount only when none of them answered, so mounting a frontend at / cannot shadow an API. Middleware still applies, and so do the guards of the router the mount was registered on, which is what lets a frontend sit behind the same authentication as everything else.

ui := muzak.NewRouter(muzak.WithDependencies(core.RequireSession))
ui.Frontend("/", muzak.FrontendOptions{Dir: "dist"})
app.Include(ui)

Mounting under a prefix works the way everything else does, through the router the mount is registered on:

ui := muzak.NewRouter()
ui.Frontend("/", muzak.FrontendOptions{Dir: "dist"})
app.Include(ui, muzak.WithPrefix("/app"))

Serving a frontend

app.Frontend("/", muzak.FrontendOptions{Dir: "dist"})

A request for a path with no file behind it falls back to one, resolved from what the build actually produced:

  1. A 404.html in the frontend's root is served with 404.
  2. Failing that, an index.html is served with 200, but only for a GET or HEAD that asks for HTML, which is what a browser navigation does.

That second rule is what a client-side router needs in order to take over. The restriction to navigations is what keeps a missing script or stylesheet answering 404, because handing those an HTML document only turns a missing file into a confusing parse error somewhere further from the cause.

OptionEffect
DirThe directory holding the build, or the subdirectory within FS when both are set
FSServe from an io/fs.FS rather than from disk
FallbackName the file served with 200 for a navigation, instead of resolving it
NotFoundName the file served with 404. It takes precedence over Fallback and applies to any GET or HEAD
NoFallbackServe a plain 404 for anything with no file behind it
SkipCheckDo not verify the directory when the application is built, for one something else fills in later

Shipping the frontend inside the binary

//go:embed all:dist
var assets embed.FS

func main() {
    app := muzak.New(muzak.AppOptions{Title: "Awesome API"})
    app.Include(routers.Items())

    app.Frontend("/", muzak.FrontendOptions{FS: assets, Dir: "dist"})

    log.Fatal(app.RunSignals())
}

Setting both reads Dir as a subdirectory of FS, which is what the directory embed above produces. The deployment is then one file.

Use all:dist rather than dist, or the embed skips files whose names begin with . or _, which is exactly what several build tools emit.

Serving assets

app.Static("/static", muzak.StaticOptions{Dir: "static"})

Static is the same machinery without the part that makes a frontend work: nothing stands in for a path with no file behind it, so a miss is a 404 and stays one.

OptionEffect
DirThe directory holding the files, or the subdirectory within FS
FSServe from an io/fs.FS, which is what an embed.FS of assets looks like
IndexServe a directory with the index.html inside it, as a web server does for a site of pages
SkipCheckDo not verify the directory when the application is built

Index is off by default, because a mount of scripts and stylesheets has no index and asking for a directory is a mistake worth reporting.

Reach for Static to publish assets, and for Frontend to serve an application whose routing happens in the browser.

What both refuse

A request that......gets
asks for a directorynever a listing. A Static mount answers its index.html only when Index asked for it
follows a symbolic link out of the mounted directoryrefused
uses a method other than GET or HEAD on a file that exists405, rather than the file
arrives before a SkipCheck directory exists500, with the reason logged

A directory that does not exist is reported when the application is built rather than on the first request, unless SkipCheck asked otherwise.

A single binary, front to back

awesome-api/
├── cmd/
│   └── main.go
├── dist/               what the frontend build wrote
│   ├── index.html
│   ├── 404.html
│   └── assets/
├── static/             assets that belong to no route
│   └── logo.svg
├── routers/
├── handlers/
├── schemas/
└── core/
app.Include(routers.Users())
app.Include(routers.Items())

app.Static("/static", muzak.StaticOptions{Dir: "static"})
app.Frontend("/", muzak.FrontendOptions{Dir: "dist"})

GET /users/ reaches the API. GET /static/logo.svg reaches the asset. GET /dashboard reaches no route and no file, so index.html is served with 200 and the browser's router takes it from there. GET /assets/app-4f2a.js with nothing behind it answers 404, so a stale hashed asset fails loudly instead of returning HTML.

Where to go next

Compression covers making those assets smaller, and Server Configuration covers deploying the binary that holds them.

Open source under MIT / Apache-2.0 · sustained by the people who ship on it.Built on net/http, and nothing else.