Single-Page Applications (SPA)
Okapi can serve a single-page application (React, Vue, Svelte, Angular, …) alongside your API. Real files (the index document, JS, CSS, images) are served directly, and any unmatched path falls back to the SPA index so the client-side router can take over.
Two entry points are provided, both taking an optional WebConfig:
Web(prefix, dir, cfg...)— serve from a directory on disk.WebFS(prefix, fsys, cfg...)— serve from anyfs.FS, including anembed.FSfor single-binary deployments.
Register the handler after your API routes so the API keeps precedence.
The
SPA/SPAFS/SPAConfignames are deprecated aliases ofWeb/WebFS/WebConfigand continue to work unchanged.
Serve from disk
Useful during front-end development:
func main() {
o := okapi.Default()
o.Get("/api/v1/users", listUsers)
// Serves ./web/index.html for "/", "/login", "/users/42", ...
o.Web("/", "./web")
if err := o.Start(); err != nil {
panic(err)
}
}
Serve from an embedded filesystem
The recommended approach for production — the whole front-end ships inside the binary:
//go:embed all:web/dist
var dist embed.FS
func main() {
o := okapi.Default()
o.Get("/api/v1/users", listUsers)
o.WebFS("/", dist, okapi.WebConfig{
Root: "web/dist", // sub-directory inside the embed.FS
MaxAge: time.Hour, // Cache-Control for assets
})
if err := o.Start(); err != nil {
panic(err)
}
}
WebFS checks at registration that the index document exists inside the
configured Root, and logs an error if it does not — the app would otherwise
answer 404 for every client-side route.
How routing works
The handler is registered for GET and HEAD under the prefix. For every
request it does not exclude:
- If the path maps to a real file, that file is served.
- Otherwise the SPA index document is returned so the client-side router can handle the route.
Registered API routes always win the match. In addition, the top-level path
segment of every registered route is auto-excluded from the fallback, so
an unknown path under an API namespace returns 404 instead of silently
serving the index. For example, with /api/v1/users registered,
/api/v1/missing returns 404 rather than index.html. An excluded path is
answered by your NoRoute handler if you have set one, and by the standard
404 otherwise.
Caching
- The index document is always served with
Cache-Control: no-cache, so a new deploy is picked up immediately. - Asset files honour
WebConfig.MaxAge. WithMaxAge: time.Hour, assets are served withCache-Control: public, max-age=3600. WhenMaxAgeis zero, noCache-Controlheader is added for assets.
Configuration
WebConfig is optional — the zero value serves index.html and
auto-excludes registered API routes.
| Field | Description |
|---|---|
Index | File served for client-side routes. Defaults to index.html. |
Root | Sub-directory inside the fs.FS that holds the built app (WebFS only; ignored by Web). |
Exclude | Additional path prefixes that must never fall back to the index. |
DisableAutoExclude | Turn off auto-excluding registered route segments; only Exclude is consulted. |
MaxAge | Cache-Control max-age for asset files. The index is always no-cache. |
Excluding extra paths
o.Web("/", "./web", okapi.WebConfig{
Exclude: []string{"/metrics", "/healthz"},
})
A full, runnable example lives in
examples/web.