Mach

Recipes

Common patterns and examples for building Mach applications

Custom 404 Handler

Since Mach uses http.ServeMux internally, 404s are handled by default. However, you can create a custom catch-all route if you need specific 404 logic (like returning a JSON error instead of text).

To handle 404s within a group (e.g., API), use a wildcard at the end of your group definitions.

app.GET("/{path...}", func(c *mach.Context) {
    c.JSON(404, map[string]string{
        "error": "resource not found",
    })
})

Graceful Shutdown

Mach supports graceful shutdown configuration, allowing ongoing requests to complete before the server stops.

// 10 seconds timeout for graceful shutdown
app.Run(":8080", mach.WithGracefulShutdown(10*time.Second))

Server Timeouts

Protect your server from slow clients by configuring read and write timeouts.

app.Run(":8080",
    mach.WithReadTimeout(5*time.Second),
    mach.WithWriteTimeout(10*time.Second),
)

Serving Single Page Applications (SPA)

To serve an SPA (like React, Vue, Svelte) where all non-file routes should redirect to index.html:

// Serve static assets from build directory
app.Static("/assets/", "./dist/assets")

// Catch-all route to serve index.html for client-side routing
app.GET("/{path...}", func(c *mach.Context) {
    http.ServeFile(c.Response, c.Request, "./dist/index.html")
})

Structuring Large Projects

For larger applications, separation of concerns is key.

cmd/
  api/
    main.go
internal/
  handlers/
    user.go
    auth.go
  middleware/
    auth.go
  models/
    user.go

In main.go:

func main() {
    app := mach.Default()

    // Register routes using handlers from internal packages
    handlers.RegisterUserRoutes(app.Group("/users"))
    handlers.RegisterAuthRoutes(app.Group("/auth"))

    app.Run(":8080")
}

Error Handling

Structured JSON Errors

Return consistent error responses across your API:

type ErrorResponse struct {
    Error   string `json:"error"`
    Code    string `json:"code,omitempty"`
    Details any    `json:"details,omitempty"`
}

func handleError(c *mach.Context, status int, message string) {
    c.JSON(status, ErrorResponse{Error: message})
}

// Usage
if user == nil {
    handleError(c, 404, "User not found")
    return
}

Panic Recovery with Custom Handler

Beyond the built-in Recovery middleware, handle panics gracefully:

app.Use(func(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                log.Printf("Panic recovered: %v", err)
                w.Header().Set("Content-Type", "application/json")
                w.WriteHeader(http.StatusInternalServerError)
                json.NewEncoder(w).Encode(map[string]string{
                    "error": "Internal server error",
                })
            }
        }()
        next.ServeHTTP(w, r)
    })
})

Rate Limiting

Simple in-memory rate limiting middleware:

func RateLimiter(requests int, window time.Duration) mach.MiddlewareFunc {
    type client struct {
        count     int
        resetTime time.Time
    }
    
    var (
        clients = make(map[string]*client)
        mu      sync.Mutex
    )
    
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            ip := r.RemoteAddr
            
            mu.Lock()
            c, exists := clients[ip]
            now := time.Now()
            
            if !exists || now.After(c.resetTime) {
                clients[ip] = &client{1, now.Add(window)}
                mu.Unlock()
                next.ServeHTTP(w, r)
                return
            }
            
            if c.count >= requests {
                mu.Unlock()
                http.Error(w, "Too many requests", http.StatusTooManyRequests)
                return
            }
            
            c.count++
            mu.Unlock()
            next.ServeHTTP(w, r)
        })
    }
}

// Usage
app.Use(RateLimiter(100, time.Minute))

Request Logging

JSON Structured Logging

import (
    "encoding/json"
    "time"
)

type logEntry struct {
    Method   string        `json:"method"`
    Path     string        `json:"path"`
    Duration time.Duration `json:"duration_ms"`
}

func JSONLogger() mach.MiddlewareFunc {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            start := time.Now()
            
            next.ServeHTTP(w, r)
            
            entry := logEntry{
                Method:   r.Method,
                Path:     r.URL.Path,
                Duration: time.Since(start),
            }
            
            if data, err := json.Marshal(entry); err == nil {
                log.Println(string(data))
            }
        })
    }
}

Health Checks

Simple health and readiness endpoints:

app.GET("/health", func(c *mach.Context) {
    c.JSON(200, map[string]string{"status": "ok"})
})

app.GET("/ready", func(c *mach.Context) {
    // Add database, cache checks here
    c.JSON(200, map[string]bool{
        "database": true,
        "cache":    true,
    })
})

Configuration

Environment-Based Config

type Config struct {
    Port         string
    ReadTimeout  time.Duration
    WriteTimeout time.Duration
    EnableDebug bool
}

func LoadConfig() *Config {
    return &Config{
        Port:         getEnv("PORT", "8080"),
        ReadTimeout:  getDurationEnv("READ_TIMEOUT", 30*time.Second),
        WriteTimeout: getDurationEnv("WRITE_TIMEOUT", 30*time.Second),
        EnableDebug:  getEnv("DEBUG", "false") == "true",
    }
}

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

For more complete examples, check out the _examples folder in the repository.

On this page