Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.go-mizu.dev/llms.txt

Use this file to discover all available pages before exploring further.

The frontend middleware offers extensive configuration options to handle various deployment scenarios, frameworks, and requirements.

Basic Configuration

Minimal Setup

The simplest configuration:
app.Use(frontend.New("./dist"))
This uses sensible defaults:
  • Auto-detects mode based on MIZU_ENV
  • Serves from ./dist in production
  • Proxies to http://localhost:5173 in development
  • Ignores /api, /health, /metrics

With Dev Server

Specify both production and development:
app.Use(frontend.WithOptions(frontend.Options{
    Mode:      frontend.ModeAuto,
    Root:      "./dist",
    DevServer: "http://localhost:5173",
}))

Embedded Filesystem

Use embedded files for single-binary deployment:
//go:embed all:dist
var distFS embed.FS

func setup() {
    dist, _ := fs.Sub(distFS, "dist")
    app.Use(frontend.WithFS(dist))
}

Options Reference

Mode

Controls how the middleware operates.
type Mode string

const (
    ModeDev        Mode = "dev"         // Always proxy to dev server
    ModeProduction Mode = "production"  // Always serve static files
    ModeAuto       Mode = "auto"        // Auto-detect from env
)
Usage:
app.Use(frontend.WithOptions(frontend.Options{
    Mode: frontend.ModeAuto,  // Default
}))
Auto-detection logic: Environment variable checked in order:
  1. MIZU_ENV
  2. GO_ENV
  3. ENV
If value is "production" or "prod" (case-insensitive) → Production mode Otherwise → Development mode

Root

Directory containing built frontend files (production only).
app.Use(frontend.WithOptions(frontend.Options{
    Root: "./dist",  // Default
}))
Path resolution:
  • Relative paths are relative to working directory
  • Absolute paths work as expected
Common paths:
  • Vite: "./dist"
  • Angular: "./dist/my-app/browser"
  • Next.js: "./out"
  • Nuxt: "./dist"

FS (Embedded Filesystem)

Embedded filesystem for production builds (takes precedence over Root).
//go:embed all:dist
var distFS embed.FS

func setup() {
    dist, _ := fs.Sub(distFS, "dist")

    app.Use(frontend.WithOptions(frontend.Options{
        FS: dist,  // Use embedded files
    }))
}
Why use fs.Sub? The embed directive includes the directory name:
distFS contains:
  dist/
    index.html
    assets/
Using fs.Sub extracts the subdirectory:
dist contains:
  index.html
  assets/
Now index.html is at the root level as expected.

Index

The fallback HTML file for SPA routing.
app.Use(frontend.WithOptions(frontend.Options{
    Index: "index.html",  // Default
}))
When is it used?
  • Request path is / or empty
  • Requested file doesn’t exist (SPA fallback)
  • Requested path is a directory

DevServer

URL of the frontend development server.
app.Use(frontend.WithOptions(frontend.Options{
    DevServer: "http://localhost:5173",  // Default (Vite)
}))
Common dev server URLs:
  • Vite: "http://localhost:5173"
  • Angular: "http://localhost:4200"
  • Next.js: "http://localhost:3000"
  • Create React App: "http://localhost:3000"

DevServerTimeout

Timeout for requests to the dev server.
app.Use(frontend.WithOptions(frontend.Options{
    DevServerTimeout: 30 * time.Second,  // Default
}))
Increase for slow dev servers:
app.Use(frontend.WithOptions(frontend.Options{
    DevServerTimeout: 60 * time.Second,
}))

ProxyWebSocket

Enable WebSocket proxying for HMR.
app.Use(frontend.WithOptions(frontend.Options{
    ProxyWebSocket: true,  // Default
}))
Disable if you don’t need HMR:
app.Use(frontend.WithOptions(frontend.Options{
    ProxyWebSocket: false,
}))

Prefix

URL prefix for serving the frontend.
app.Use(frontend.WithOptions(frontend.Options{
    Prefix: "/app",  // Serve from /app/*
}))
Routing:
  • /app → index.html
  • /app/about → index.html (SPA fallback)
  • /app/assets/main.js → assets/main.js
  • / → Not handled by frontend middleware
Frontend router configuration required:
// React Router
<BrowserRouter basename="/app">
// Vite config
export default {
    base: '/app/',
}

IgnorePaths

Paths that bypass the frontend middleware and go to Go handlers.
app.Use(frontend.WithOptions(frontend.Options{
    IgnorePaths: []string{"/api", "/health", "/metrics"},  // Default
}))
Add custom paths:
app.Use(frontend.WithOptions(frontend.Options{
    IgnorePaths: []string{"/api", "/auth", "/webhooks"},
}))
Disable all:
app.Use(frontend.WithOptions(frontend.Options{
    IgnorePaths: []string{},  // No automatic ignores
}))

CacheControl

Configure caching behavior for different asset types.
app.Use(frontend.WithOptions(frontend.Options{
    CacheControl: frontend.CacheConfig{
        HashedAssets:   365 * 24 * time.Hour,  // Default: 1 year
        UnhashedAssets: 7 * 24 * time.Hour,    // Default: 1 week
        HTML:           0,                      // Default: no-cache
        Patterns: map[string]time.Duration{
            "*.woff2": 30 * 24 * time.Hour,
        },
    },
}))
CacheConfig fields:
  • HashedAssets: Files with content hash in name (e.g., main.abc123.js)
  • UnhashedAssets: Files without hash (e.g., logo.png)
  • HTML: HTML files (.html)
  • Patterns: Custom patterns override defaults
See Caching Strategy for details.

SecurityHeaders

Enable automatic security headers.
app.Use(frontend.WithOptions(frontend.Options{
    SecurityHeaders: true,  // Default
}))
Headers added:
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
Referrer-Policy: strict-origin-when-cross-origin
Disable if using helmet:
app.Use(helmet.Default())

app.Use(frontend.WithOptions(frontend.Options{
    SecurityHeaders: false,  // Avoid duplicates
}))

SourceMaps

Allow serving source map files (.js.map, .css.map).
app.Use(frontend.WithOptions(frontend.Options{
    SourceMaps: false,  // Default (blocked in production)
}))
Enable for debugging:
app.Use(frontend.WithOptions(frontend.Options{
    SourceMaps: true,
}))
Conditional source maps:
isProduction := os.Getenv("MIZU_ENV") == "production"

app.Use(frontend.WithOptions(frontend.Options{
    SourceMaps: !isProduction,  // Only in development
}))

Manifest

Path to build manifest for asset mapping.
app.Use(frontend.WithOptions(frontend.Options{
    Manifest: ".vite/manifest.json",
}))
The manifest is used for:
  • Asset fingerprinting
  • Module preloading
  • CSS injection
  • Template helpers
See Build Manifest for details.

InjectEnv

Inject server-side environment variables into the frontend.
app.Use(frontend.WithOptions(frontend.Options{
    InjectEnv: []string{"API_URL", "ANALYTICS_ID"},
}))
Variables are exposed as window.__ENV__:
// Frontend code
const apiUrl = window.__ENV__.API_URL;
Security warning: Only inject non-sensitive values. These are visible to users. See Environment Injection for details.

InjectMeta

Inject custom meta tags into HTML.
app.Use(frontend.WithOptions(frontend.Options{
    InjectMeta: map[string]string{
        "description": "My awesome app",
        "keywords":    "go, mizu, web",
    },
}))
Injects:
<meta name="description" content="My awesome app">
<meta name="keywords" content="go, mizu, web">

ServiceWorker

Path to service worker file for PWA support.
app.Use(frontend.WithOptions(frontend.Options{
    ServiceWorker: "sw.js",
}))
Proper headers are added for service worker scope. See Service Workers for details.

ErrorHandler

Custom error handler for frontend errors.
app.Use(frontend.WithOptions(frontend.Options{
    ErrorHandler: func(c *mizu.Ctx, err error) error {
        c.Logger().Error("frontend error", "error", err)
        return c.JSON(500, map[string]string{
            "error": "Internal Server Error",
        })
    },
}))

NotFoundHandler

Custom handler called before SPA fallback.
app.Use(frontend.WithOptions(frontend.Options{
    NotFoundHandler: func(c *mizu.Ctx) error {
        // Log 404s
        c.Logger().Warn("file not found", "path", c.Request().URL.Path)

        // Return nil to continue to SPA fallback
        return nil

        // Or return error to skip fallback and respond
        // return c.JSON(404, map[string]string{"error": "not found"})
    },
}))

Configuration Examples

Complete Production Setup

package server

import (
    "embed"
    "io/fs"
    "os"
    "time"

    "github.com/go-mizu/mizu"
    "github.com/go-mizu/mizu/frontend"
)

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

func New() *mizu.App {
    app := mizu.New()

    // API routes first
    setupAPIRoutes(app)

    // Frontend configuration
    dist, _ := fs.Sub(distFS, "dist")
    app.Use(frontend.WithOptions(frontend.Options{
        // Mode
        Mode: frontend.ModeProduction,

        // Files
        FS:    dist,
        Index: "index.html",

        // Routing
        Prefix:      "",
        IgnorePaths: []string{"/api"},

        // Caching
        CacheControl: frontend.CacheConfig{
            HashedAssets:   365 * 24 * time.Hour,
            UnhashedAssets: 7 * 24 * time.Hour,
            HTML:           0,
            Patterns: map[string]time.Duration{
                "*.woff2": 30 * 24 * time.Hour,
            },
        },

        // Security
        SecurityHeaders: true,
        SourceMaps:      false,

        // Environment
        InjectEnv: []string{"API_URL"},
        InjectMeta: map[string]string{
            "description": "My app",
        },
    }))

    return app
}

Development-Optimized Setup

func New() *mizu.App {
    app := mizu.New()

    setupAPIRoutes(app)

    app.Use(frontend.WithOptions(frontend.Options{
        Mode:             frontend.ModeDev,
        DevServer:        "http://localhost:5173",
        DevServerTimeout: 60 * time.Second,
        ProxyWebSocket:   true,
        IgnorePaths:      []string{"/api"},
    }))

    return app
}

Auto-Switching Setup

func New() *mizu.App {
    app := mizu.New()

    setupAPIRoutes(app)

    dist, _ := fs.Sub(distFS, "dist")
    app.Use(frontend.WithOptions(frontend.Options{
        // Auto-detect based on MIZU_ENV
        Mode: frontend.ModeAuto,

        // Production settings
        FS:   dist,
        Root: "./dist",  // Fallback if FS fails

        // Development settings
        DevServer:      "http://localhost:5173",
        ProxyWebSocket: true,

        // Common settings
        IgnorePaths: []string{"/api"},
        InjectEnv:   []string{"API_URL"},
    }))

    return app
}

Multi-Environment Setup

type Config struct {
    Environment string
    Port        string
    DevPort     string
}

func New(cfg *Config) *mizu.App {
    app := mizu.New()

    setupAPIRoutes(app)

    opts := frontend.Options{
        Mode:        frontend.ModeAuto,
        Root:        "./dist",
        DevServer:   "http://localhost:" + cfg.DevPort,
        IgnorePaths: []string{"/api"},
    }

    // Environment-specific configuration
    switch cfg.Environment {
    case "production":
        dist, _ := fs.Sub(distFS, "dist")
        opts.FS = dist
        opts.SourceMaps = false
        opts.SecurityHeaders = true
        opts.InjectEnv = []string{"API_URL"}

    case "staging":
        opts.SourceMaps = true  // Enable for debugging
        opts.SecurityHeaders = true
        opts.InjectEnv = []string{"API_URL", "DEBUG"}

    case "development":
        opts.Mode = frontend.ModeDev
        opts.DevServerTimeout = 60 * time.Second
    }

    app.Use(frontend.WithOptions(opts))

    return app
}

Environment Variables

Configure via environment:
# Mode detection
export MIZU_ENV=production  # or development
export GO_ENV=production    # alternative
export ENV=production       # alternative

# Custom ports
export PORT=3000
export DEV_PORT=5173

# API URL injection
export API_URL=https://api.example.com
Access in configuration:
type Config struct {
    Env     string
    Port    string
    DevPort string
    APIURL  string
}

func LoadConfig() *Config {
    return &Config{
        Env:     getEnv("MIZU_ENV", "development"),
        Port:    getEnv("PORT", "3000"),
        DevPort: getEnv("DEV_PORT", "5173"),
        APIURL:  getEnv("API_URL", "http://localhost:3000/api"),
    }
}

func getEnv(key, fallback string) string {
    if value := os.Getenv(key); value != "" {
        return value
    }
    return fallback
}

Next Steps

Development Mode

Learn about dev mode and HMR

Production Mode

Optimize for production deployment

Caching Strategy

Deep dive into caching

Security

Security best practices