Skip to main content

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