Example 06 - Handler Configurations

Complete runnable example demonstrating handler configurations: SPA mounting and static file serving.

Note: Reverse proxy examples require external services and are documented separately in the full README. This runnable example focuses on SPA and static file serving.

What’s Demonstrated

File Structure

06-handlers/
├── main.go                 # Application entry
├── user_service.go         # Business service
├── user_repository.go      # Repository
├── config.yaml             # Handler configurations
├── go.mod
├── test.http
├── dist/                   # SPA builds
│   ├── admin/
│   │   └── index.html     # Admin dashboard SPA
│   └── landing/
│       └── index.html     # Landing page SPA
├── public/                 # Static assets
│   └── assets/
│       ├── logo.svg       # Logo image
│       └── style.css      # Stylesheet
└── README.md

Configuration Highlights

SPA Mounts

mount-spa:
  # Admin dashboard at /admin
  - prefix: "/admin"
    dir: "./dist/admin"
  
  # Landing page at root
  - prefix: "/"
    dir: "./dist/landing"

Behavior:

Examples:

Static File Mounts

mount-static:
  # Public assets
  - prefix: "/assets"
    dir: "./public/assets"

Behavior:

Examples:

Handler Mount Order

Handlers are mounted in this priority:

  1. Business Routers - /api/users (from published-services)
  2. SPA Mounts - /admin/*, /*
  3. Static Mounts - /assets/*

Important: Most specific routes first!

apps:
  - addr: ":8080"
    published-services: [user-service]  # 1st: /api/users
    mount-spa:
      - prefix: "/admin"                 # 2nd: /admin/*
      - prefix: "/"                      # 3rd: /* (catch-all)
    mount-static:
      - prefix: "/assets"                # 4th: /assets/*

Run

# First time: generate code
go run .

# The application will start on :8080

Test

1. Test Business API

curl http://localhost:8080/api/users

2. Test Admin SPA

# Root
curl http://localhost:8080/admin

# Client-side route (still serves index.html)
curl http://localhost:8080/admin/users

# Or open in browser
open http://localhost:8080/admin

3. Test Landing Page

curl http://localhost:8080/

# Or open in browser
open http://localhost:8080/

4. Test Static Assets

# Logo
curl http://localhost:8080/assets/logo.svg

# CSS
curl http://localhost:8080/assets/style.css

Or use test.http in VS Code with REST Client extension.

How It Works

1. SPA Mounting

When you access /admin/users:

  1. Request arrives at router
  2. No exact match for /admin/users
  3. Falls through to SPA handler at /admin
  4. Handler checks if /users is a file → NO
  5. Serves dist/admin/index.html
  6. Client-side router (React/Vue/Angular) handles /users route

2. Static File Serving

When you access /assets/logo.svg:

  1. Request arrives at router
  2. Falls through to static handler at /assets
  3. Handler serves public/assets/logo.svg directly

3. Routing Priority

Request: GET /api/users
  ✓ Matches business router → Handle

Request: GET /admin
  ✗ No match in business router
  ✓ Matches SPA at /admin → Serve index.html

Request: GET /assets/logo.svg
  ✗ No match in business router
  ✗ No match in SPA
  ✓ Matches static at /assets → Serve file

Request: GET /unknown
  ✗ No match in business router
  ✗ No match in specific SPAs
  ✓ Matches root SPA at / → Serve index.html (404 page)

Architecture Patterns

Pattern 1: API + Admin SPA

apps:
  - addr: ":8080"
    published-services: [api-service]
    mount-spa:
      - prefix: "/admin"
        dir: "./dist/admin"

Use case: Admin dashboard alongside API

Pattern 2: API + Multiple SPAs

apps:
  - addr: ":8080"
    published-services: [api-service]
    mount-spa:
      - prefix: "/admin"
        dir: "./dist/admin"
      - prefix: "/app"
        dir: "./dist/app"
      - prefix: "/"
        dir: "./dist/landing"

Use case: Multi-tenant or multi-app platform

Pattern 3: SPA + Static Assets

apps:
  - addr: ":8080"
    mount-spa:
      - prefix: "/"
        dir: "./dist/spa"
    mount-static:
      - prefix: "/assets"
        dir: "./public/assets"

Use case: Pure frontend app with static assets

Production Considerations

1. SPA Caching

Problem: index.html should never be cached Solution: Add middleware

// Add no-cache headers for HTML
func noCacheMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if strings.HasSuffix(r.URL.Path, ".html") {
            w.Header().Set("Cache-Control", "no-cache, no-repository, must-revalidate")
        }
        next.ServeHTTP(w, r)
    })
}

2. Static Asset Caching

Problem: Assets should be cached for performance Solution: Use versioned filenames

<!-- Good: versioned assets -->
<script src="/assets/app.12345.js"></script>

<!-- Bad: no versioning -->
<script src="/assets/app.js"></script>

3. Compression

Add gzip middleware for better performance:

middleware-definitions:
  gzip:
    type: gzip-compression

4. CDN for Production

For production, consider using CDN for static assets:

Summary

This example demonstrates how to:

All configured declaratively in YAML without writing handler code! 🎉

Next Steps

What’s Demonstrated

Configuration Highlights

1. Reverse Proxy

Simple proxy with prefix stripping:

reverse-proxies:
  - prefix: "/api/v2"
    strip-prefix: true
    target: "http://backend:9000"

Request flow:

Client: GET /api/v2/users
  ↓ (strip-prefix: true)
Backend: GET /users

With path rewriting:

reverse-proxies:
  - prefix: "/graphql"
    target: "http://graphql-server:4000"
    rewrite:
      from: "^/graphql"
      to: "/api/graphql"

Request flow:

Client: POST /graphql
  ↓ (rewrite)
Backend: POST /api/graphql

2. SPA Mounting

mount-spa:
  - prefix: "/admin"
    dir: "./dist/admin-spa"

Behavior:

Use cases:

3. Static File Serving

mount-static:
  - prefix: "/assets"
    dir: "./public/assets"

Behavior:

Use cases:

Architecture Patterns

API Gateway Pattern

apps:
  - addr: ":8080"
    reverse-proxies:
      - prefix: "/api/users"
        target: "http://user-service:9001"
      - prefix: "/api/orders"
        target: "http://order-service:9002"
      - prefix: "/api/payments"
        target: "http://payment-service:9003"

Backend-for-Frontend (BFF)

apps:
  - addr: ":8080"
    published-services: [bff-service]
    reverse-proxies:
      - prefix: "/internal/users"
        target: "http://user-service:9001"
      - prefix: "/internal/orders"
        target: "http://order-service:9002"
    mount-spa:
      - prefix: "/"
        dir: "./dist/web-app"

Monolith + SPA

apps:
  - addr: ":8080"
    published-services: [api-service]
    mount-spa:
      - prefix: "/"
        dir: "./dist/spa"

CDN Server

apps:
  - addr: ":8081"
    mount-static:
      - prefix: "/images"
        dir: "./cdn/images"
      - prefix: "/videos"
        dir: "./cdn/videos"

Request Routing Order

Handlers are mounted in this order:

  1. Reverse Proxies (prepended first)
  2. Business Routers (from routers and published-services)
  3. SPA Mounts (added after business routers)
  4. Static Mounts (added after SPA mounts)

Example:

apps:
  - addr: ":8080"
    reverse-proxies: [...]      # 1st priority
    routers: [user-router]       # 2nd priority
    published-services: [...]    # 2nd priority (auto-generated routers)
    mount-spa: [...]             # 3rd priority
    mount-static: [...]          # 4th priority

Run

go run main.go

Test Reverse Proxy

# Test API v2 proxy (strips /api/v2 prefix)
curl http://localhost:8080/api/v2/users

# Test GraphQL proxy (rewrites path)
curl -X POST http://localhost:8080/graphql \
  -H "Content-Type: application/json" \
  -d '{"query": "{ users { id name } }"}'

Test SPA Mount

# All these serve index.html
curl http://localhost:8080/admin
curl http://localhost:8080/admin/users
curl http://localhost:8080/admin/settings

# Static files served directly
curl http://localhost:8080/admin/logo.png
curl http://localhost:8080/admin/app.js

Test Static Files

# Serve file directly
curl http://localhost:8080/assets/logo.png

# Directory → index.html
curl http://localhost:8080/assets/css/

Production Considerations

1. Reverse Proxy

Pros:

Cons:

Best for:

2. SPA Mounting

Pros:

Cons:

Best practices:

apps:
  - addr: ":8080"
    # Add compression middleware
    routers: [compression-router]
    mount-spa:
      - prefix: "/"
        dir: "./dist/spa"

3. Static Files

Pros:

Cons:

Best for:

For production CDN: Consider using dedicated services like Cloudflare, AWS CloudFront, or nginx.

Summary

This example demonstrates how to configure various handlers at the app level without writing any Go code. All routing, proxying, and file serving is configured declaratively in YAML! 🎉