Clone
5
HWS
Haelnorr edited this page 2026-01-13 21:22:53 +11:00

HWS - v0.2.3

A lightweight, opinionated HTTP web server framework for Go built on top of the standard library's net/http.

Overview

HWS (H Web Server) provides a structured approach to building HTTP servers in Go. It leverages Go 1.22+ routing patterns, provides built-in middleware for common tasks, and offers a clean API for handling errors and logging.

Installation

go get git.haelnorr.com/h/golib/hws

Key Concepts

Design Philosophy

  1. Built on Standards: Uses net/http with Go 1.22+ routing patterns
  2. Opinionated Defaults: Sensible defaults for timeouts, logging, and compression
  3. Structured Errors: Type-safe error handling with customizable error pages
  4. Middleware First: Built-in middleware for timing, logging, and compression
  5. Production Ready: Health checks, graceful shutdown, and timeout controls

Server Lifecycle

  1. Create server with configuration
  2. Configure logger, error pages, and ignored paths
  3. Define routes with methods and handlers
  4. Add routes to server
  5. Add middleware (required before start)
  6. Start server with context
  7. Wait for server to be ready
  8. Shutdown gracefully when done

Quick Start

Basic Server

package main

import (
    "context"
    "git.haelnorr.com/h/golib/hws"
    "net/http"
)

func main() {
    // Create server
    server, err := hws.NewServer(&hws.Config{
        Host: "127.0.0.1",
        Port: 3000,
        GZIP: true,
    })
    if err != nil {
        panic(err)
    }

    // Define routes
    routes := []hws.Route{
        {
            Path:    "/",
            Method:  hws.MethodGET,
            Handler: http.HandlerFunc(homeHandler),
        },
    }

    // Add routes and middleware
    server.AddRoutes(routes...)
    server.AddMiddleware()

    // Start server
    ctx := context.Background()
    server.Start(ctx)

    // Wait for ready
    <-server.Ready()
    
    // Block forever
    select {}
}

func homeHandler(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Hello, World!"))
}

Configuration

Config Struct

type Config struct {
    Host              string        // Host to listen on
    Port              uint64        // Port to listen on
    TrustedHost       string        // Trusted hostname/domain
    GZIP              bool          // Enable GZIP compression
    ReadHeaderTimeout time.Duration // Timeout for reading headers
    WriteTimeout      time.Duration // Timeout for writing responses
    IdleTimeout       time.Duration // Timeout for idle connections
}

Using Environment Variables

Load configuration from environment variables with ConfigFromEnv():

config, err := hws.ConfigFromEnv()
if err != nil {
    return err
}

server, err := hws.NewServer(config)

Environment variables:

Variable Type Default Description
HWS_HOST string 127.0.0.1 Host to bind to
HWS_PORT uint64 3000 Port to listen on
HWS_TRUSTED_HOST string Same as HWS_HOST Trusted hostname
HWS_GZIP bool false Enable GZIP compression
HWS_READ_HEADER_TIMEOUT int 2 Header read timeout (seconds)
HWS_WRITE_TIMEOUT int 10 Write timeout (seconds)
HWS_IDLE_TIMEOUT int 120 Idle timeout (seconds)

Manual Configuration

config := &hws.Config{
    Host:              "0.0.0.0",
    Port:              8080,
    TrustedHost:       "example.com",
    GZIP:              true,
    ReadHeaderTimeout: 5 * time.Second,
    WriteTimeout:      30 * time.Second,
    IdleTimeout:       120 * time.Second,
}

server, err := hws.NewServer(config)

Default Values

If fields are not specified, the following defaults are applied:

  • Host: 127.0.0.1
  • Port: 3000
  • TrustedHost: Same as Host
  • GZIP: false
  • ReadHeaderTimeout: 2 seconds
  • WriteTimeout: 10 seconds
  • IdleTimeout: 120 seconds

Routing

Route Definition

Routes use Go 1.22+ routing patterns with explicit HTTP methods:

type Route struct {
    Path    string       // URL path pattern
    Method  Method       // HTTP method
    Handler http.Handler // Request handler
}

HTTP Methods

Available method constants:

hws.MethodGET
hws.MethodPOST
hws.MethodPUT
hws.MethodPATCH
hws.MethodDELETE
hws.MethodHEAD
hws.MethodOPTIONS
hws.MethodCONNECT
hws.MethodTRACE

Basic Routes

routes := []hws.Route{
    {
        Path:    "/",
        Method:  hws.MethodGET,
        Handler: http.HandlerFunc(homeHandler),
    },
    {
        Path:    "/about",
        Method:  hws.MethodGET,
        Handler: http.HandlerFunc(aboutHandler),
    },
    {
        Path:    "/contact",
        Method:  hws.MethodPOST,
        Handler: http.HandlerFunc(contactHandler),
    },
}

err := server.AddRoutes(routes...)

Path Parameters

Use Go 1.22+ path parameters:

routes := []hws.Route{
    {
        Path:    "/users/{id}",
        Method:  hws.MethodGET,
        Handler: http.HandlerFunc(getUserHandler),
    },
    {
        Path:    "/posts/{post_id}/comments/{comment_id}",
        Method:  hws.MethodGET,
        Handler: http.HandlerFunc(getCommentHandler),
    },
}

func getUserHandler(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    w.Write([]byte("User ID: " + id))
}

func getCommentHandler(w http.ResponseWriter, r *http.Request) {
    postID := r.PathValue("post_id")
    commentID := r.PathValue("comment_id")
    w.Write([]byte(fmt.Sprintf("Post: %s, Comment: %s", postID, commentID)))
}

Wildcard Routes

routes := []hws.Route{
    {
        Path:    "/static/",
        Method:  hws.MethodGET,
        Handler: http.StripPrefix("/static/", staticHandler),
    },
    {
        Path:    "/api/",
        Method:  hws.MethodGET,
        Handler: apiHandler,
    },
}

REST API Routes

routes := []hws.Route{
    // List users
    {
        Path:    "/api/users",
        Method:  hws.MethodGET,
        Handler: http.HandlerFunc(listUsersHandler),
    },
    // Create user
    {
        Path:    "/api/users",
        Method:  hws.MethodPOST,
        Handler: http.HandlerFunc(createUserHandler),
    },
    // Get user
    {
        Path:    "/api/users/{id}",
        Method:  hws.MethodGET,
        Handler: http.HandlerFunc(getUserHandler),
    },
    // Update user
    {
        Path:    "/api/users/{id}",
        Method:  hws.MethodPUT,
        Handler: http.HandlerFunc(updateUserHandler),
    },
    // Delete user
    {
        Path:    "/api/users/{id}",
        Method:  hws.MethodDELETE,
        Handler: http.HandlerFunc(deleteUserHandler),
    },
}

Middleware

Middleware Execution Order

Middleware runs in the following order:

  1. Timer - Records request start time (automatic)
  2. Custom middleware - Your middleware in the order provided
  3. GZIP - Compresses responses if enabled (automatic)
  4. Logging - Logs request details (automatic)
  5. Route handler - Your route handler

Adding Middleware

// Custom middleware functions
authMiddleware := createAuthMiddleware()
corsMiddleware := createCORSMiddleware()

// Add in order (they execute in reverse order of addition)
err := server.AddMiddleware(authMiddleware, corsMiddleware)

Creating Middleware - Method 1: Using NewMiddleware

server.NewMiddleware creates middleware from a MiddlewareFunc:

type MiddlewareFunc func(w http.ResponseWriter, r *http.Request) (*http.Request, *HWSError)

This approach provides structured error handling:

// Authentication middleware
authMiddleware := server.NewMiddleware(func(w http.ResponseWriter, r *http.Request) (*http.Request, *hws.HWSError) {
    token := r.Header.Get("Authorization")
    if token == "" {
        return r, &hws.HWSError{
            StatusCode:      http.StatusUnauthorized,
            Message:         "Missing authorization token",
            Error:           errors.New("no auth token"),
            Level:           hws.ErrorWARN,
            RenderErrorPage: true, // Stop request chain and render error page
        }
    }
    
    // Validate token (simplified)
    userID, err := validateToken(token)
    if err != nil {
        return r, &hws.HWSError{
            StatusCode:      http.StatusUnauthorized,
            Message:         "Invalid token",
            Error:           err,
            Level:           hws.ErrorWARN,
            RenderErrorPage: true,
        }
    }
    
    // Add user to context
    ctx := context.WithValue(r.Context(), "userID", userID)
    return r.WithContext(ctx), nil
})

server.AddMiddleware(authMiddleware)

Creating Middleware - Method 2: Standard net/http

Use standard net/http middleware pattern:

func corsMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Access-Control-Allow-Origin", "*")
        w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
        w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
        
        if r.Method == "OPTIONS" {
            w.WriteHeader(http.StatusOK)
            return
        }
        
        next.ServeHTTP(w, r)
    })
}

server.AddMiddleware(corsMiddleware)

Middleware Examples

Request ID Middleware

requestIDMiddleware := server.NewMiddleware(func(w http.ResponseWriter, r *http.Request) (*http.Request, *hws.HWSError) {
    requestID := uuid.New().String()
    w.Header().Set("X-Request-ID", requestID)
    
    ctx := context.WithValue(r.Context(), "requestID", requestID)
    return r.WithContext(ctx), nil
})

Rate Limiting Middleware

func rateLimitMiddleware(limiter *rate.Limiter) hws.Middleware {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            if !limiter.Allow() {
                http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
                return
            }
            next.ServeHTTP(w, r)
        })
    }
}

limiter := rate.NewLimiter(10, 100) // 10 req/sec, burst of 100
server.AddMiddleware(rateLimitMiddleware(limiter))

Security Headers Middleware

securityHeadersMiddleware := func(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("X-Content-Type-Options", "nosniff")
        w.Header().Set("X-Frame-Options", "DENY")
        w.Header().Set("X-XSS-Protection", "1; mode=block")
        w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
        next.ServeHTTP(w, r)
    })
}

server.AddMiddleware(securityHeadersMiddleware)

Recovery Middleware

recoveryMiddleware := func(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                server.LogError(hws.HWSError{
                    StatusCode: http.StatusInternalServerError,
                    Message:    "Panic recovered",
                    Error:      fmt.Errorf("panic: %v", err),
                    Level:      hws.ErrorFATAL,
                })
                http.Error(w, "Internal Server Error", http.StatusInternalServerError)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

server.AddMiddleware(recoveryMiddleware)

Error Handling

HWSError Type

type HWSError struct {
    StatusCode      int        // HTTP status code
    Message         string     // Error message
    Error           error      // Underlying error
    Level           ErrorLevel // Log level (default: ErrorERROR)
    RenderErrorPage bool       // Render error page and stop chain
}

Error Levels

hws.ErrorDEBUG  // Debug information
hws.ErrorINFO   // Informational messages
hws.ErrorWARN   // Warning conditions
hws.ErrorERROR  // Error conditions (default)
hws.ErrorFATAL  // Fatal errors (terminates program)
hws.ErrorPANIC  // Panic-level errors

Custom Error Pages

Implement the ErrorPage interface:

type ErrorPage interface {
    Render(ctx context.Context, w io.Writer) error
}

Register error page function:

// Using templ or any template engine
server.AddErrorPage(func(errorCode int) (hws.ErrorPage, error) {
    return errorPages.Error(errorCode), nil
})

// Manual implementation
type CustomErrorPage struct {
    Code int
}

func (e *CustomErrorPage) Render(ctx context.Context, w io.Writer) error {
    html := fmt.Sprintf(`
        <!DOCTYPE html>
        <html>
        <head><title>Error %d</title></head>
        <body>
            <h1>Error %d</h1>
            <p>Something went wrong.</p>
        </body>
        </html>
    `, e.Code, e.Code)
    
    _, err := w.Write([]byte(html))
    return err
}

server.AddErrorPage(func(errorCode int) (hws.ErrorPage, error) {
    return &CustomErrorPage{Code: errorCode}, nil
})

Throwing Errors

func getUserHandler(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    
    user, err := database.GetUser(id)
    if err != nil {
        server.ThrowError(w, r, hws.HWSError{
            StatusCode:      http.StatusNotFound,
            Message:         "User not found",
            Error:           err,
            Level:           hws.ErrorWARN,
            RenderErrorPage: true, // Renders error page and stops
        })
        return
    }
    
    // Success response
    json.NewEncoder(w).Encode(user)
}

Error Without Rendering Page

Use RenderErrorPage: false to log error but continue:

server.ThrowError(w, r, hws.HWSError{
    StatusCode:      http.StatusInternalServerError,
    Message:         "Failed to send email notification",
    Error:           err,
    Level:           hws.ErrorWARN,
    RenderErrorPage: false, // Log only, don't render error page
})

// Continue with response
w.Write([]byte("User created, but email notification failed"))

Fatal Errors

server.ThrowFatal(w, err) // Logs fatal error and terminates

Logging

Adding a Logger

import "git.haelnorr.com/h/golib/hlog"

logger, err := hlog.NewLogger(&hlog.Config{
    Level:       "info",
    PrettyPrint: true,
})
if err != nil {
    return err
}

err = server.AddLogger(logger)
if err != nil {
    return err
}

Ignoring Paths

Prevent logging for specific paths:

err := server.LoggerIgnorePaths(
    "/static/",
    "/favicon.ico",
    "/healthz",
)

What Gets Logged

The logging middleware automatically captures:

  • Request method (GET, POST, etc.)
  • Request path
  • Response status code
  • Request duration
  • Client IP address
  • Error details (if any)

Example log output:

{"level":"info","method":"GET","path":"/api/users/123","status":200,"duration":"45ms","ip":"192.168.1.1","time":"2026-01-11T10:30:00Z","message":"Request completed"}

{"level":"warn","method":"POST","path":"/api/users","status":400,"duration":"12ms","ip":"192.168.1.1","error":"invalid email","time":"2026-01-11T10:30:05Z","message":"User validation failed"}

Manual Logging

Use the logger directly in handlers:

func handler(w http.ResponseWriter, r *http.Request) {
    logger.Info("Processing user request")
    
    server.LogError(hws.HWSError{
        StatusCode: http.StatusBadRequest,
        Message:    "Invalid input",
        Error:      errors.New("missing field"),
        Level:      hws.ErrorWARN,
    })
}

Static File Serving

SafeFileServer

SafeFileServer prevents directory listing by returning 404 for directory requests:

import "embed"

//go:embed static/*
var staticFS embed.FS

func setupStatic(server *hws.Server) error {
    httpFS := http.FS(staticFS)
    staticHandler, err := hws.SafeFileServer(&httpFS)
    if err != nil {
        return err
    }
    
    routes := []hws.Route{
        {
            Path:    "/static/",
            Method:  hws.MethodGET,
            Handler: http.StripPrefix("/static/", staticHandler),
        },
    }
    
    return server.AddRoutes(routes...)
}

Using OS Filesystem

fs := http.Dir("./public")
httpFS := http.FileSystem(fs)
staticHandler, err := hws.SafeFileServer(&httpFS)
if err != nil {
    return err
}

routes := []hws.Route{
    {
        Path:    "/assets/",
        Method:  hws.MethodGET,
        Handler: http.StripPrefix("/assets/", staticHandler),
    },
}

Ignoring Static File Logs

server.LoggerIgnorePaths("/static/", "/assets/", "/favicon.ico")

Server Lifecycle

Starting the Server

// Start the server (runs in background goroutine)
logger.Debug().Msg("Starting up the HTTP server")
err := server.Start(ctx)
if err != nil {
    return errors.Wrap(err, "server.Start")
}

The Start method:

  1. Validates that routes have been added
  2. Applies middleware if not already done
  3. Starts the HTTP server in a background goroutine
  4. Returns immediately, allowing your code to continue
  5. Server listens for context cancellation to shutdown

Checking Server Status

// Wait for server to be ready
<-server.Ready()

// Check if ready without blocking
if server.IsReady() {
    fmt.Println("Server is ready")
}

// Get server address
addr := server.Addr() // e.g., "127.0.0.1:3000"

Graceful Shutdown

import (
    "context"
    "os"
    "os/signal"
    "sync"
    "time"
)

func run() error {
    // Create context that listens for interrupt signals
    ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
    defer cancel()
    
    // ... server setup ...
    
    // Start server
    err := server.Start(ctx)
    if err != nil {
        return err
    }
    
    logger.Info().Msgf("Server started on %s", server.Addr())
    
    // Handle graceful shutdown
    var wg sync.WaitGroup
    wg.Go(func() {
        <-ctx.Done()
        
        shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
        defer cancel()
        
        if err := server.Shutdown(shutdownCtx); err != nil {
            logger.Error().Err(err).Msg("Graceful shutdown failed")
        }
    })
    
    wg.Wait()
    logger.Info().Msg("Server shutdown complete")
    return nil
}

Health Check

The server automatically provides a health check endpoint at /healthz:

curl http://localhost:3000/healthz
# Returns: 200 OK (no body)

This endpoint:

  • Always returns 200 OK when server is running
  • Is used internally to determine server readiness
  • Should be ignored in logging: server.LoggerIgnorePaths("/healthz")

Complete Production Example

package main

import (
    "context"
    "database/sql"
    "embed"
    "fmt"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"

    "git.haelnorr.com/h/golib/hlog"
    "git.haelnorr.com/h/golib/hws"
    "git.haelnorr.com/h/golib/hwsauth"
    _ "github.com/lib/pq"
)

//go:embed static/*
var staticFS embed.FS

func main() {
    if err := run(); err != nil {
        fmt.Fprintf(os.Stderr, "Error: %v\n", err)
        os.Exit(1)
    }
}

func run() error {
    // Load configuration
    config, err := hws.ConfigFromEnv()
    if err != nil {
        return err
    }
    
    // Create server
    server, err := hws.NewServer(config)
    if err != nil {
        return err
    }
    
    // Setup logger
    logger, err := hlog.NewLogger(&hlog.Config{
        Level:       "info",
        PrettyPrint: os.Getenv("ENV") != "production",
    })
    if err != nil {
        return err
    }
    
    server.AddLogger(logger)
    server.LoggerIgnorePaths("/static/", "/favicon.ico", "/healthz")
    
    // Setup error pages
    server.AddErrorPage(func(errorCode int) (hws.ErrorPage, error) {
        return &ErrorPage{Code: errorCode}, nil
    })
    
    // Setup database
    db, err := sql.Open("postgres", os.Getenv("DATABASE_URL"))
    if err != nil {
        return err
    }
    defer db.Close()
    
    // Setup static files
    httpFS := http.FS(staticFS)
    staticHandler, err := hws.SafeFileServer(&httpFS)
    if err != nil {
        return err
    }
    
    // Define routes
    routes := []hws.Route{
        // Static files
        {
            Path:    "/static/",
            Method:  hws.MethodGET,
            Handler: http.StripPrefix("/static/", staticHandler),
        },
        
        // Public pages
        {
            Path:    "/",
            Method:  hws.MethodGET,
            Handler: http.HandlerFunc(homeHandler),
        },
        {
            Path:    "/about",
            Method:  hws.MethodGET,
            Handler: http.HandlerFunc(aboutHandler),
        },
        
        // API endpoints
        {
            Path:    "/api/users",
            Method:  hws.MethodGET,
            Handler: http.HandlerFunc(listUsersHandler(db)),
        },
        {
            Path:    "/api/users/{id}",
            Method:  hws.MethodGET,
            Handler: http.HandlerFunc(getUserHandler(db)),
        },
        {
            Path:    "/api/users",
            Method:  hws.MethodPOST,
            Handler: http.HandlerFunc(createUserHandler(server, db)),
        },
    }
    
    // Register routes
    if err := server.AddRoutes(routes...); err != nil {
        return err
    }
    
    // Add custom middleware
    corsMiddleware := createCORSMiddleware()
    securityMiddleware := createSecurityHeadersMiddleware()
    
    if err := server.AddMiddleware(corsMiddleware, securityMiddleware); err != nil {
        return err
    }
    
    // Start server
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    
    if err := server.Start(ctx); err != nil {
        return err
    }
    
    logger.Info(fmt.Sprintf("Server started on %s", server.Addr()))
    
    // Wait for interrupt signal
    sigChan := make(chan os.Signal, 1)
    signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
    <-sigChan
    
    logger.Info("Shutting down server...")
    
    // Graceful shutdown
    shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer shutdownCancel()
    
    if err := server.Shutdown(shutdownCtx); err != nil {
        return fmt.Errorf("shutdown failed: %w", err)
    }
    
    logger.Info("Server shutdown complete")
    return nil
}

func homeHandler(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Welcome to the API"))
}

func aboutHandler(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("About this API"))
}

func listUsersHandler(db *sql.DB) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        // Implementation
    }
}

func getUserHandler(db *sql.DB) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        id := r.PathValue("id")
        // Implementation
    }
}

func createUserHandler(server *hws.Server, db *sql.DB) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        // Implementation with error handling
        if err := validateInput(r); err != nil {
            server.ThrowError(w, r, hws.HWSError{
                StatusCode:      http.StatusBadRequest,
                Message:         "Invalid input",
                Error:           err,
                Level:           hws.ErrorWARN,
                RenderErrorPage: false,
            })
            return
        }
        // ... create user ...
    }
}

func createCORSMiddleware() hws.Middleware {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            w.Header().Set("Access-Control-Allow-Origin", "*")
            w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
            w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
            
            if r.Method == "OPTIONS" {
                w.WriteHeader(http.StatusOK)
                return
            }
            
            next.ServeHTTP(w, r)
        })
    }
}

func createSecurityHeadersMiddleware() hws.Middleware {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            w.Header().Set("X-Content-Type-Options", "nosniff")
            w.Header().Set("X-Frame-Options", "DENY")
            w.Header().Set("X-XSS-Protection", "1; mode=block")
            next.ServeHTTP(w, r)
        })
    }
}

type ErrorPage struct {
    Code int
}

func (e *ErrorPage) Render(ctx context.Context, w io.Writer) error {
    html := fmt.Sprintf(`<!DOCTYPE html>
<html>
<head>
    <title>Error %d</title>
    <style>
        body { font-family: sans-serif; text-align: center; padding: 50px; }
        h1 { color: #e74c3c; }
    </style>
</head>
<body>
    <h1>Error %d</h1>
    <p>Something went wrong. Please try again later.</p>
</body>
</html>`, e.Code, e.Code)
    
    _, err := w.Write([]byte(html))
    return err
}

Integration

With ezconf

HWS includes built-in integration with ezconf for unified configuration management:

import (
	"git.haelnorr.com/h/golib/ezconf"
	"git.haelnorr.com/h/golib/hlog"
	"git.haelnorr.com/h/golib/hws"
)

func main() {
	// Create ezconf loader
	loader := ezconf.New()

	// Register packages
	loader.RegisterIntegrations(
		hws.NewEZConfIntegration(),
		hlog.NewEZConfIntegration(),
	)

	// Load all configurations
	if err := loader.Load(); err != nil {
		log.Fatal(err)
	}

	// Get configurations
	hwsCfg, _ := loader.GetConfig("hws")
	cfg := hwsCfg.(*hws.Config)

	// Create server with configuration
	server := hws.NewServer(cfg)
	server.Start()
}

Benefits of using ezconf:

  • Unified configuration across multiple packages
  • Automatic environment variable documentation
  • Generate and manage .env files

See the ezconf documentation for more details.

With hwsauth

hws integrates seamlessly with hwsauth for JWT authentication:

import "git.haelnorr.com/h/golib/hwsauth"

// Setup authentication
authConfig, _ := hwsauth.ConfigFromEnv()
auth, _ := hwsauth.NewAuthenticator[*User, bun.Tx](
    authConfig,
    loadUserFunc,
    server,
    beginTxFunc,
    logger,
    errorPageFunc,
)

// Apply authentication middleware
server.AddMiddleware(auth.Authenticate())

// Ignore public paths
auth.IgnorePaths("/", "/static/", "/login", "/register")

// Protected routes
routes := []hws.Route{
    {
        Path:    "/dashboard",
        Method:  hws.MethodGET,
        Handler: auth.LoginReq(dashboardHandler),
    },
    {
        Path:    "/change-password",
        Method:  hws.MethodPOST,
        Handler: auth.LoginReq(auth.FreshReq(changePasswordHandler)),
    },
}

See the hwsauth documentation for details.

Best Practices

Configuration

  • Use environment variables for production configuration
  • Set appropriate timeouts for your use case
  • Enable GZIP for production to reduce bandwidth

Routing

  • Use RESTful patterns for API endpoints
  • Leverage path parameters for dynamic routes
  • Group related routes together

Middleware

  • Add recovery middleware to catch panics
  • Use rate limiting for public endpoints
  • Apply security headers for all responses
  • Order middleware carefully (auth before business logic)

Error Handling

  • Always provide meaningful error messages
  • Use appropriate HTTP status codes
  • Set RenderErrorPage: false for API endpoints
  • Log errors with appropriate levels

Logging

  • Ignore static asset paths to reduce noise
  • Use structured logging with context
  • Don't log sensitive information
  • Monitor error rates and patterns

Static Files

  • Always use SafeFileServer to prevent directory listing
  • Serve static files from embedded FS in production
  • Ignore static paths in logging
  • Use caching headers for static assets

Lifecycle

  • Implement graceful shutdown
  • Handle OS signals properly
  • Wait for server readiness before tests
  • Set reasonable shutdown timeouts

Security

  • Validate all user input
  • Use HTTPS in production
  • Set security headers
  • Implement rate limiting
  • Use CORS appropriately

Troubleshooting

Server won't start

Check that:

  • Routes have been added with AddRoutes()
  • Middleware has been added with AddMiddleware()
  • Port is not already in use
  • Host is valid (IP or hostname)

Routes not matching

  • Ensure HTTP method matches exactly
  • Check path pattern syntax (Go 1.22+ patterns)
  • Verify route was added before starting server
  • Check middleware isn't blocking requests

Middleware not executing

  • Verify AddMiddleware() was called after AddRoutes()
  • Check middleware order (they execute in reverse)
  • Ensure middleware calls next.ServeHTTP()

Error pages not rendering

  • Verify AddErrorPage() was called
  • Check RenderErrorPage: true in HWSError
  • Ensure ErrorPage.Render() writes to the writer
  • Look for errors in error page rendering logic

Logs not appearing

  • Check logger was added with AddLogger()
  • Verify log level in hlog configuration
  • Check if path is in ignored paths list
  • Ensure request completes (middleware chain)

See Also

  • HWSAuth - JWT authentication middleware
  • JWT - JWT token management
  • hlog - Structured logging