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.

Overview

The spa middleware serves Single Page Applications by returning the index file for any unmatched routes, enabling client-side routing to work correctly. Use it when you need:
  • React/Vue/Angular app hosting
  • Client-side routing support
  • HTML5 pushState routing

Installation

import "github.com/go-mizu/mizu/middlewares/spa"

Quick Start

app := mizu.New()

// Serve SPA from ./dist directory
app.Use(spa.New("./dist"))

// API routes still work
app.Get("/api/users", listUsers)

Configuration

Options

OptionTypeDefaultDescription
RootstringRequiredRoot directory
FSfs.FSnilFilesystem to serve from
Indexstring"index.html"Index file name
Prefixstring""URL prefix
SkipPaths[]string[]Paths to skip (pass through)

Examples

Basic Usage

app.Use(spa.New("./dist"))

From Embedded FS

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

app.Use(spa.WithFS(distFS, "dist"))

Skip API Routes

app.Use(spa.WithOptions(spa.Options{
    Root:      "./dist",
    SkipPaths: []string{"/api", "/health"},
}))

Custom Index

app.Use(spa.WithOptions(spa.Options{
    Root:  "./public",
    Index: "app.html",
}))

With Prefix

app.Use(spa.WithOptions(spa.Options{
    Root:   "./dist",
    Prefix: "/app",
}))
// SPA available at /app/*

How It Works

  1. Request comes in for /users/123
  2. Check if file exists at ./dist/users/123
  3. If file exists: serve it
  4. If not: serve ./dist/index.html
  5. Client-side router handles /users/123

API Reference

Functions

// New creates SPA middleware for directory
func New(root string) mizu.Middleware

// WithFS creates SPA middleware for fs.FS
func WithFS(fsys fs.FS, root string) mizu.Middleware

// WithOptions creates SPA middleware with configuration
func WithOptions(opts Options) mizu.Middleware

Complete Example

package main

import (
    "embed"
    "github.com/go-mizu/mizu"
    "github.com/go-mizu/mizu/middlewares/spa"
)

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

func main() {
    app := mizu.New()

    // API routes
    api := app.Group("/api")
    api.Get("/users", listUsers)
    api.Post("/users", createUser)

    // Serve SPA for everything else
    app.Use(spa.WithOptions(spa.Options{
        FS:        distFS,
        Root:      "dist",
        SkipPaths: []string{"/api"},
    }))

    app.Listen(":3000")
}

Technical Details

Implementation Overview

The SPA middleware provides a fallback mechanism for single-page applications by serving the index file when a requested file does not exist. This enables client-side routing to work correctly.

Path Resolution Process

  1. Ignore Path Check: Checks if the request path matches any configured IgnorePaths (default: /api)
  2. Prefix Handling: If a Prefix is configured, strips it from the path before file resolution
  3. File Existence Check: Determines if the requested path corresponds to an actual file:
    • For fs.FS: Uses fs.Stat() to check file existence
    • For directory path: Uses os.Stat() with filepath.Join()
  4. File Serving: If file exists and is not a directory, serves it with appropriate cache headers
  5. Index Fallback: If file doesn’t exist, serves the index file (default: index.html)

Cache Control Strategy

The middleware implements a dual cache control strategy:
  • Static Assets: Configurable via MaxAge option (default: 0/no cache)
    • Sets Cache-Control: public, max-age=<MaxAge> header
    • Recommended: Long cache duration (e.g., 31536000 for 1 year) for versioned assets
  • Index File: Configurable via IndexMaxAge option (default: 0)
    • When IndexMaxAge = 0: Sets Cache-Control: no-cache, no-store, must-revalidate
    • When IndexMaxAge > 0: Sets Cache-Control: public, max-age=<IndexMaxAge>
    • No-cache default ensures users always get latest SPA routing configuration

File Serving Details

The middleware uses http.ServeContent() for file delivery, which provides:
  • Automatic MIME type detection based on file extension
  • HTTP range request support for partial content
  • Proper Last-Modified header handling
  • Efficient byte streaming with io.ReadSeeker

Security Considerations

  • All file paths are cleaned using filepath.Clean() to prevent directory traversal attacks
  • The middleware only serves files from the configured root directory or filesystem
  • Directory listings are disabled (directories trigger index fallback)

Best Practices

  • Define API routes before SPA middleware
  • Use embedded FS for portable deployments
  • Skip API and health check paths
  • Set proper cache headers for static assets
  • Use long cache durations for static assets with content hashing
  • Keep index.html uncached to ensure SPA updates are immediately available

Testing

The SPA middleware includes comprehensive test coverage for all configuration scenarios:
Test CaseDescriptionExpected Behavior
Serve static fileRequest for existing static file (/app.js)Returns file content with status 200
Fallback to index for routeRequest for non-existent route (/users/123)Returns index.html content with status 200
Root pathRequest for root path (/)Returns index.html content with status 200
Serve from FSRequest for file in embedded FS (/main.js)Returns file content from fs.FS
Fallback from FSRequest for non-existent file in FS (/dashboard)Returns index.html from fs.FS
Ignore API pathRequest to ignored path (/api/users)Passes through to registered route handler
Ignore health pathRequest to ignored path (/health)Passes through to registered route handler
Non-ignored path gets SPARequest to non-ignored path (/profile)Returns SPA index file
With prefixRequest with configured prefix (/app/dashboard)Strips prefix and serves SPA
Without prefixRequest without prefix when prefix configured (/other)Passes through to other handlers
Static asset cacheRequest for static file with MaxAge setReturns file with Cache-Control: public, max-age=31536000
Index no cacheRequest for route with IndexMaxAge=0Returns index with Cache-Control: no-cache, no-store, must-revalidate
Custom indexRequest with custom index file (app.html)Returns custom index instead of index.html
Panics without FS or RootCreate middleware with empty optionsPanics with error message