Table of Contents
- HWS - v0.2.3
- Overview
- Installation
- Key Concepts
- Quick Start
- Configuration
- Routing
- Middleware
- Middleware Execution Order
- Adding Middleware
- Creating Middleware - Method 1: Using NewMiddleware
- Creating Middleware - Method 2: Standard net/http
- Middleware Examples
- Error Handling
- HWSError Type
- Error Levels
- Custom Error Pages
- Throwing Errors
- Error Without Rendering Page
- Fatal Errors
- Logging
- Static File Serving
- Server Lifecycle
- Complete Production Example
- Integration
- Best Practices
- Troubleshooting
- Server won't start
- Routes not matching
- Middleware not executing
- Error pages not rendering
- Logs not appearing
- See Also
- Links
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
- Built on Standards: Uses
net/httpwith Go 1.22+ routing patterns - Opinionated Defaults: Sensible defaults for timeouts, logging, and compression
- Structured Errors: Type-safe error handling with customizable error pages
- Middleware First: Built-in middleware for timing, logging, and compression
- Production Ready: Health checks, graceful shutdown, and timeout controls
Server Lifecycle
- Create server with configuration
- Configure logger, error pages, and ignored paths
- Define routes with methods and handlers
- Add routes to server
- Add middleware (required before start)
- Start server with context
- Wait for server to be ready
- 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.1Port:3000TrustedHost: Same asHostGZIP:falseReadHeaderTimeout:2 secondsWriteTimeout:10 secondsIdleTimeout: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:
- Timer - Records request start time (automatic)
- Custom middleware - Your middleware in the order provided
- GZIP - Compresses responses if enabled (automatic)
- Logging - Logs request details (automatic)
- 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:
- Validates that routes have been added
- Applies middleware if not already done
- Starts the HTTP server in a background goroutine
- Returns immediately, allowing your code to continue
- 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 OKwhen 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: falsefor 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
SafeFileServerto 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 afterAddRoutes() - Check middleware order (they execute in reverse)
- Ensure middleware calls
next.ServeHTTP()
Error pages not rendering
- Verify
AddErrorPage()was called - Check
RenderErrorPage: trueinHWSError - 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
Links
- GoDoc - API documentation
- Source Code - Repository
- Issue Tracker - Report bugs
- Examples - Code examples