package web
import (
"embed"
"io/fs"
)
//go:embed dist
var distFS embed.FS
// Assets returns the embedded frontend rooted at the dist directory,
// so request paths map directly onto file names.
func Assets() fs.FS {
sub, err := fs.Sub(distFS, "dist")
if err != nil {
// embed guarantees dist exists at build time; a failure here is a bug.
panic(err)
}
return sub
}
package web
import (
"crypto/sha256"
"encoding/hex"
"io"
"io/fs"
"net/http"
"path"
"strings"
)
type SPAHandler struct {
fsys fs.FS
files http.Handler
}
func NewSPAHandler(fsys fs.FS) *SPAHandler {
return &SPAHandler{fsys: fsys, files: http.FileServer(http.FS(fsys))}
}
func (h *SPAHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
name := strings.TrimPrefix(path.Clean(r.URL.Path), "/")
if name == "" {
name = "index.html"
}
if _, err := fs.Stat(h.fsys, name); err != nil {
// Unknown path: let the SPA router handle it via the shell.
name = "index.html"
r.URL.Path = "/index.html"
}
if name == "index.html" {
w.Header().Set("Cache-Control", "no-cache")
} else {
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
if tag, err := contentHash(h.fsys, name); err == nil {
w.Header().Set("ETag", tag)
}
}
h.files.ServeHTTP(w, r)
}
func contentHash(fsys fs.FS, name string) (string, error) {
f, err := fsys.Open(name)
if err != nil {
return "", err
}
defer f.Close()
sum := sha256.New()
if _, err := io.Copy(sum, f); err != nil {
return "", err
}
return `"` + hex.EncodeToString(sum.Sum(nil)[:8]) + `"`, nil
}
package main
import (
"log"
"net/http"
"example.com/app/web"
)
func main() {
mux := http.NewServeMux()
// More specific patterns must be registered before the catch-all.
mux.HandleFunc("/api/health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"status":"ok"}`))
})
mux.Handle("/", web.NewSPAHandler(web.Assets()))
srv := &http.Server{Addr: ":8080", Handler: mux}
log.Println("listening on :8080")
if err := srv.ListenAndServe(); err != nil {
log.Fatal(err)
}
}
This snippet shows how a single Go binary can ship its own frontend by embedding static files at compile time with the embed package and serving them over net/http. The pattern removes the need to deploy a separate public/ directory alongside the binary, which simplifies containers and eliminates a whole class of "file not found in production" bugs.
In assets.go, the //go:embed dist directive captures the entire dist directory into an embed.FS named distFS. Because the embedded tree includes the top-level dist folder, fs.Sub is used to re-root the filesystem so paths line up with request URLs like /index.html rather than /dist/index.html. The resulting fs.FS is exposed through Assets() so the HTTP layer never touches the raw embed variable directly.
static_handler.go wraps that filesystem in a handler with real-world behavior. http.FS adapts an fs.FS into an http.FileSystem so http.FileServer can serve it. The key detail is the fallback logic: single-page apps route client-side, so any path that does not resolve to a real embedded file is rewritten to index.html before serving, letting the frontend router handle it. contentHash computes a short sha256-derived ETag per file so browsers can revalidate cheaply; hashed asset filenames get a long immutable cache lifetime while index.html is marked no-cache to avoid serving a stale shell that points at deleted bundles. This split is the standard trade-off for SPA deploys — cache the fingerprinted files aggressively, never cache the entry point.
main.go ties it together, mounting the asset handler under / while keeping an API prefix separate, and demonstrates that ordering matters: more specific mux patterns win, so /api/ is registered before the catch-all. A subtle pitfall worth noting is that embed.FS always uses forward slashes and is read-only, so callers must not rely on OS path separators or attempt writes. The approach fits any tool or service that wants a zero-dependency, self-contained deployment.
Related snips
package api
import (
"net/http"
"runtime/debug"
)
Expose build metadata for debugging deploys
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
module Api
module V1
class UsersController < BaseController
def show
user = User.includes(:profile).find(params[:id])
ETags for conditional requests and caching
package dbutil
import (
"context"
"github.com/jackc/pgconn"
Retry Postgres serialization failures with bounded attempts
json.array! @posts do |post|
json.cache! ['v1', post], expires_in: 1.hour do
json.id post.id
json.title post.title
json.excerpt post.excerpt
json.published_at post.published_at
Fragment caching for expensive JSON serialization
package files
import (
"context"
"time"
Presigned S3 upload URLs (AWS SDK v2)
Share this code
Here's the card — post it anywhere.