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.goIn 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.