This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

HTTP Router

An HTTP router for Go. Built for cloud-native applications with complete routing, middleware, and observability features.

The Rivaas Router provides a high-performance routing system. Includes built-in middleware, OpenTelemetry support, and complete request handling.

Overview

The Rivaas Router is a production-ready HTTP router for cloud-native applications. It combines high performance with a rich feature set. It delivers 8.4M+ requests per second at 119ns per operation. It includes automatic request binding, validation, content negotiation, API versioning, and native OpenTelemetry tracing.

Key Features

Core Routing & Request Handling

  • Radix tree routing - Path matching with bloom filters for static route lookups.
  • Compiled route tables - Pre-compiled routes for static and dynamic path matching.
  • Path Parameters: /users/:id, /posts/:id/:action - Array-based storage for route parameters.
  • Wildcard Routes: /files/*filepath - Catch-all routing for file serving.
  • Route Groups: Organize routes with shared prefixes and middleware.
  • Middleware Chain: Global, group-level, and route-level middleware support.
  • Route Constraints: Numeric, UUID, Alpha, Alphanumeric, Custom regex validation.
  • Concurrent Safe: Thread-safe for use by multiple goroutines.

Request Binding

Automatically bind request data to structs:

  • Router Context: Built-in BindStrict() for strict JSON binding with size limits.
  • Binding Package: Full binding with binding.Query(), binding.JSON(), binding.Form(), binding.Headers(), binding.Cookies().
  • 15+ Type Categories: Primitives, Time, Network types like net.IP and net.IPNet, Maps, Nested Structs, Slices.
  • Advanced Features: Maps with dot or bracket notation, nested structs in query strings, enum validation, default values.

Request Validation

  • Multiple Strategies: Interface validation, Tag validation with go-playground/validator, JSON Schema.
  • Partial Validation: PATCH request support. Validate only present fields.
  • Structured Errors: Machine-readable error codes and field paths.
  • Context-Aware: Request-scoped validation rules.

Response Rendering

  • JSON Variants: Standard, Indented, Pure, Secure, ASCII, JSONP.
  • Alternative Formats: YAML, String, HTML.
  • Binary & Streaming: Zero-copy streaming from io.Reader, file serving.

Content Negotiation - RFC 7231 Compliant

  • Media type negotiation with quality values.
  • Character set, encoding, and language negotiation.
  • Wildcard support and specificity matching.

API Versioning - Built-in

  • Header-based: API-Version: v1
  • Query-based: ?version=v1
  • Custom detection: Flexible version strategies
  • Version-specific routes: r.Version("v1").GET(...)
  • Lock-free implementation: Atomic operations

Middleware (Built-in)

  • AccessLog - Structured HTTP access logging
  • Recovery - Panic recovery with graceful errors
  • CORS - Cross-Origin Resource Sharing
  • Basic Auth - HTTP Basic Authentication
  • Compression - Gzip/Brotli response compression
  • Request ID - X-Request-ID generation
  • Security Headers - HSTS, CSP, X-Frame-Options
  • Timeout - Request timeout handling
  • Rate Limit - Token bucket rate limiting
  • Body Limit - Request body size limiting

Observability - OpenTelemetry Native

  • Metrics: Custom histograms, counters, gauges, automatic request metrics
  • Tracing: Native OpenTelemetry support with zero overhead when disabled
  • Diagnostics: Optional diagnostic events for security concerns

Performance

  • Sub-microsecond routing: 119ns per operation
  • High throughput: 8.4M+ requests/second
  • Memory efficient: 16 bytes per request, 1 allocation
  • Context pooling: Automatic context reuse
  • Lock-free operations: Atomic operations for concurrent access

Quick Start

Get up and running in minutes with this complete example:

package main

import (
    "fmt"
    "net/http"
    "time"
    
    "rivaas.dev/router"
)

func main() {
    r := router.MustNew()  // Panics on invalid config (use at startup)
    
    // Global middleware
    r.Use(Logger(), Recovery())
    
    // Simple route
    r.GET("/", func(c *router.Context) {
        c.JSON(http.StatusOK, map[string]string{
            "message": "Hello Rivaas!",
            "version": "1.0.0",
        })
    })
    
    // Parameter route
    r.GET("/users/:id", func(c *router.Context) {
        userID := c.Param("id")
        c.JSON(http.StatusOK, map[string]string{
            "user_id": userID,
        })
    })
    
    // POST with strict JSON binding
    r.POST("/users", func(c *router.Context) {
        var req struct {
            Name  string `json:"name"`
            Email string `json:"email"`
        }
        
        if err := c.BindStrict(&req, router.BindOptions{MaxBytes: 1 << 20}); err != nil {
            return // Error response already written
        }
        
        c.JSON(http.StatusCreated, req)
    })
    
    http.ListenAndServe(":8080", r)
}

// Middleware examples
func Logger() router.HandlerFunc {
    return func(c *router.Context) {
        start := time.Now()
        c.Next()
        duration := time.Since(start)
        fmt.Printf("[%s] %s - %v\n", c.Request.Method, c.Request.URL.Path, duration)
    }
}

func Recovery() router.HandlerFunc {
    return func(c *router.Context) {
        defer func() {
            if err := recover(); err != nil {
                c.JSON(http.StatusInternalServerError, map[string]string{
                    "error": "Internal server error",
                })
            }
        }()
        c.Next()
    }
}

Learning Path

Follow this structured path to master the Rivaas Router:

1. Getting Started

Start with the basics:

2. Core Features

Build upon the fundamentals:

  • Route Groups - Organize routes with groups and prefixes
  • Middleware - Add cross-cutting concerns like logging and auth
  • Context - Understand the request context and memory safety

3. Request Handling

Handle requests effectively:

4. Advanced Features

Use advanced capabilities:

5. Production Readiness

Prepare for production:

  • Observability - Integrate OpenTelemetry tracing and diagnostics
  • Testing - Test your routes and middleware
  • Migration - Migrate from Gin, Echo, or http.ServeMux

6. Examples & Patterns

Learn from real-world examples:

  • Examples - Complete working examples and use cases

Common Use Cases

The Rivaas Router excels in these scenarios:

  • REST APIs - JSON APIs with comprehensive request/response handling
  • Web Applications - HTML rendering, forms, sessions, static files
  • Microservices - OpenTelemetry integration, API versioning, health checks
  • High-Performance Services - Sub-microsecond routing, 8.4M+ req/s throughput

Next Steps

Need Help?

1 - Installation

Install the Rivaas Router in your Go project.

Requirements

  • Go 1.25 or higher.
  • Standard library only. No external dependencies for core routing.

Install the Router

Add the router to your Go project:

go get rivaas.dev/router

Verify Installation

Create a simple test file to verify the installation:

package main

import (
    "net/http"
    "rivaas.dev/router"
)

func main() {
    r := router.MustNew()
    
    r.GET("/", func(c *router.Context) {
        c.String(http.StatusOK, "Router is working!")
    })
    
    http.ListenAndServe(":8080", r)
}

Run the test:

go run main.go

Visit http://localhost:8080/ in your browser - you should see “Router is working!”

Optional Dependencies

Middleware

For built-in middleware like structured logging and metrics:

# For AccessLog middleware (structured logging)
go get rivaas.dev/logging

# For Metrics middleware
go get rivaas.dev/metrics

OpenTelemetry Tracing

For OpenTelemetry tracing support:

# Core OpenTelemetry libraries
go get go.opentelemetry.io/otel
go get go.opentelemetry.io/otel/trace
go get go.opentelemetry.io/otel/sdk

# Example: Jaeger exporter
go get go.opentelemetry.io/otel/exporters/jaeger

Validation

For tag-based validation (go-playground/validator):

go get github.com/go-playground/validator/v10

The router automatically detects and uses validator if available.

Project Structure

Recommended project structure for a router-based application:

myapp/
├── main.go                 # Application entry point
├── routes/
│   ├── routes.go          # Route registration
│   ├── users.go           # User routes
│   └── posts.go           # Post routes
├── handlers/
│   ├── users.go           # User handlers
│   └── posts.go           # Post handlers
├── middleware/
│   ├── auth.go            # Authentication middleware
│   └── logging.go         # Custom logging
└── go.mod

Next Steps

2 - Basic Usage

Learn the fundamentals of the Rivaas Router - from your first router to handling requests.

This guide introduces the core concepts of the Rivaas Router through progressive examples.

Your First Router

Let’s start with the simplest possible router:

package main

import (
    "net/http"
    "rivaas.dev/router"
)

func main() {
    r := router.MustNew()  // Panics on invalid config (use at startup)
    
    r.GET("/", func(c *router.Context) {
        c.JSON(http.StatusOK, map[string]string{
            "message": "Hello, Rivaas Router!",
        })
    })
    
    http.ListenAndServe(":8080", r)
}

What’s happening here:

  1. router.MustNew() creates a new router instance. Panics on invalid config.
  2. r.GET("/", handler) registers a handler for GET requests to /.
  3. The handler function receives a *router.Context with request and response information.
  4. c.JSON() sends a JSON response.
  5. http.ListenAndServe() starts the HTTP server.

Test it:

curl http://localhost:8080/
# Output: {"message":"Hello, Rivaas Router!"}

Adding Routes with Parameters

Routes can capture dynamic segments from the URL path:

func main() {
    r := router.MustNew()
    
    // Static route
    r.GET("/", func(c *router.Context) {
        c.JSON(http.StatusOK, map[string]string{
            "message": "Welcome to Rivaas Router",
        })
    })
    
    // Single parameter
    r.GET("/users/:id", func(c *router.Context) {
        userID := c.Param("id")
        c.JSON(http.StatusOK, map[string]string{
            "user_id": userID,
            "message": "User found",
        })
    })
    
    // Multiple parameters
    r.GET("/users/:id/posts/:post_id", func(c *router.Context) {
        userID := c.Param("id")
        postID := c.Param("post_id")
        c.JSON(http.StatusOK, map[string]string{
            "user_id": userID,
            "post_id": postID,
        })
    })
    
    http.ListenAndServe(":8080", r)
}

Parameter syntax:

  • :name - Captures a path segment and stores it under the given name.
  • Access with c.Param("name").
  • Parameters match any non-slash characters.

Test it:

curl http://localhost:8080/users/123
# Output: {"user_id":"123","message":"User found"}

curl http://localhost:8080/users/123/posts/456
# Output: {"user_id":"123","post_id":"456"}

HTTP Methods

The router supports all standard HTTP methods:

func main() {
    r := router.MustNew()
    
    r.GET("/users", listUsers)          // List all users
    r.POST("/users", createUser)        // Create a new user
    r.GET("/users/:id", getUser)        // Get a specific user
    r.PUT("/users/:id", updateUser)     // Update a user (full replacement)
    r.PATCH("/users/:id", patchUser)    // Partial update
    r.DELETE("/users/:id", deleteUser)  // Delete a user
    r.HEAD("/users/:id", headUser)      // Check if user exists
    r.OPTIONS("/users", optionsUsers)   // Get available methods
    
    http.ListenAndServe(":8080", r)
}

func listUsers(c *router.Context) {
    c.JSON(200, []string{"user1", "user2"})
}

func createUser(c *router.Context) {
    c.JSON(201, map[string]string{"message": "User created"})
}

func getUser(c *router.Context) {
    c.JSON(200, map[string]string{"user_id": c.Param("id")})
}

func updateUser(c *router.Context) {
    c.JSON(200, map[string]string{"message": "User updated"})
}

func patchUser(c *router.Context) {
    c.JSON(200, map[string]string{"message": "User patched"})
}

func deleteUser(c *router.Context) {
    c.Status(204) // No Content
}

func headUser(c *router.Context) {
    c.Status(200) // OK, no body
}

func optionsUsers(c *router.Context) {
    c.Header("Allow", "GET, POST, OPTIONS")
    c.Status(200)
}

Reading Request Data

Query Parameters

Access query string parameters with c.Query():

// GET /search?q=golang&limit=10
r.GET("/search", func(c *router.Context) {
    query := c.Query("q")
    limit := c.Query("limit")
    
    c.JSON(200, map[string]string{
        "query": query,
        "limit": limit,
    })
})

Test it:

curl "http://localhost:8080/search?q=golang&limit=10"
# Output: {"query":"golang","limit":"10"}

Form Data

Access POST form data with c.FormValue():

// POST /login with form data
r.POST("/login", func(c *router.Context) {
    username := c.FormValue("username")
    password := c.FormValue("password")
    
    // Validate credentials...
    c.JSON(200, map[string]string{
        "username": username,
        "status": "logged in",
    })
})

Test it:

curl -X POST http://localhost:8080/login \
  -d "username=john" \
  -d "password=secret"
# Output: {"username":"john","status":"logged in"}

JSON Request Body

Parse JSON request bodies:

r.POST("/users", func(c *router.Context) {
    var req struct {
        Name  string `json:"name"`
        Email string `json:"email"`
    }
    
    if err := json.NewDecoder(c.Request.Body).Decode(&req); err != nil {
        c.JSON(400, map[string]string{"error": "Invalid JSON"})
        return
    }
    
    c.JSON(201, map[string]interface{}{
        "id":    "123",
        "name":  req.Name,
        "email": req.Email,
    })
})

Test it:

curl -X POST http://localhost:8080/users \
  -H "Content-Type: application/json" \
  -d '{"name":"John Doe","email":"john@example.com"}'
# Output: {"id":"123","name":"John Doe","email":"john@example.com"}

Error Handling

Always handle errors and provide meaningful responses:

r.GET("/users/:id", func(c *router.Context) {
    userID := c.Param("id")
    
    // Validate user ID
    if userID == "" {
        c.JSON(400, map[string]string{
            "error": "User ID is required",
        })
        return
    }
    
    // Simulate user lookup
    user, err := findUser(userID)
    if err != nil {
        if err == ErrUserNotFound {
            c.JSON(404, map[string]string{
                "error": "User not found",
            })
        } else {
            c.JSON(500, map[string]string{
                "error": "Internal server error",
            })
        }
        return
    }
    
    c.JSON(200, user)
})

Response Types

The router supports multiple response formats:

JSON Responses

// Standard JSON
r.GET("/json", func(c *router.Context) {
    c.JSON(200, map[string]string{"message": "JSON response"})
})

// Indented JSON (for debugging)
r.GET("/json-pretty", func(c *router.Context) {
    c.IndentedJSON(200, map[string]string{"message": "Pretty JSON"})
})

Plain Text

r.GET("/text", func(c *router.Context) {
    c.String(200, "Plain text response")
})

// With formatting
r.GET("/text-formatted", func(c *router.Context) {
    c.Stringf(200, "Hello, %s!", "World")
})

HTML

r.GET("/html", func(c *router.Context) {
    c.HTML(200, "<h1>Hello, World!</h1>")
})

Status Only

r.DELETE("/users/:id", func(c *router.Context) {
    // Delete user...
    c.Status(204) // No Content
})

Working with Headers

Reading Headers

r.GET("/headers", func(c *router.Context) {
    userAgent := c.Request.Header.Get("User-Agent")
    contentType := c.Request.Header.Get("Content-Type")
    
    c.JSON(200, map[string]string{
        "user_agent":   userAgent,
        "content_type": contentType,
    })
})

Setting Headers

r.GET("/custom-headers", func(c *router.Context) {
    c.Header("X-Custom-Header", "CustomValue")
    c.Header("Cache-Control", "no-cache")
    c.JSON(200, map[string]string{"message": "Headers set"})
})

Redirects

Redirect to another URL:

r.GET("/old-url", func(c *router.Context) {
    c.Redirect(301, "/new-url") // 301 Permanent Redirect
})

r.GET("/temporary", func(c *router.Context) {
    c.Redirect(302, "/elsewhere") // 302 Temporary Redirect
})

Complete Example

Here’s a complete example combining all the concepts:

package main

import (
    "encoding/json"
    "net/http"
    "rivaas.dev/router"
)

type User struct {
    ID    string `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

var users = map[string]User{
    "1": {ID: "1", Name: "Alice", Email: "alice@example.com"},
    "2": {ID: "2", Name: "Bob", Email: "bob@example.com"},
}

func main() {
    r := router.MustNew()
    
    // List all users
    r.GET("/users", func(c *router.Context) {
        userList := make([]User, 0, len(users))
        for _, user := range users {
            userList = append(userList, user)
        }
        c.JSON(200, userList)
    })
    
    // Get a specific user
    r.GET("/users/:id", func(c *router.Context) {
        id := c.Param("id")
        user, exists := users[id]
        if !exists {
            c.JSON(404, map[string]string{
                "error": "User not found",
            })
            return
        }
        c.JSON(200, user)
    })
    
    // Create a new user
    r.POST("/users", func(c *router.Context) {
        var req User
        if err := json.NewDecoder(c.Request.Body).Decode(&req); err != nil {
            c.JSON(400, map[string]string{
                "error": "Invalid JSON",
            })
            return
        }
        
        // Generate ID (simplified)
        req.ID = "3"
        users[req.ID] = req
        
        c.JSON(201, req)
    })
    
    // Update a user
    r.PUT("/users/:id", func(c *router.Context) {
        id := c.Param("id")
        if _, exists := users[id]; !exists {
            c.JSON(404, map[string]string{
                "error": "User not found",
            })
            return
        }
        
        var req User
        if err := json.NewDecoder(c.Request.Body).Decode(&req); err != nil {
            c.JSON(400, map[string]string{
                "error": "Invalid JSON",
            })
            return
        }
        
        req.ID = id
        users[id] = req
        c.JSON(200, req)
    })
    
    // Delete a user
    r.DELETE("/users/:id", func(c *router.Context) {
        id := c.Param("id")
        if _, exists := users[id]; !exists {
            c.JSON(404, map[string]string{
                "error": "User not found",
            })
            return
        }
        
        delete(users, id)
        c.Status(204)
    })
    
    http.ListenAndServe(":8080", r)
}

Next Steps

Now that you understand the basics:

3 - Route Patterns

Learn about static routes, parameter routes, wildcards, and route matching priority.

The Rivaas Router supports multiple route patterns, from simple static routes to dynamic parameters and wildcards.

Static Routes

Static routes match exact path strings and have the best performance:

r.GET("/", homeHandler)
r.GET("/about", aboutHandler)
r.GET("/api/health", healthHandler)
r.GET("/admin/dashboard", dashboardHandler)

Characteristics:

  • Exact string match required.
  • Fastest route type. Sub-microsecond lookups.
  • Uses hash table lookups with bloom filters.
  • No pattern matching overhead.
curl http://localhost:8080/about
# Matches: /about
# Does NOT match: /about/, /about/us

Parameter Routes

Routes can capture dynamic segments using the :param syntax:

Single Parameter

// Capture user ID
r.GET("/users/:id", func(c *router.Context) {
    userID := c.Param("id")
    c.JSON(200, map[string]string{"user_id": userID})
})

Matches:

  • /users/123id="123"
  • /users/abcid="abc"
  • /users/uuid-hereid="uuid-here"

Does NOT match:

  • /users - missing parameter
  • /users/ - empty parameter
  • /users/123/posts - too many segments

Multiple Parameters

r.GET("/users/:id/posts/:post_id", func(c *router.Context) {
    userID := c.Param("id")
    postID := c.Param("post_id")
    c.JSON(200, map[string]string{
        "user_id": userID,
        "post_id": postID,
    })
})

Matches:

  • /users/123/posts/456id="123", post_id="456"
  • /users/alice/posts/hello-worldid="alice", post_id="hello-world"

Mixed Static and Parameter Segments

r.GET("/api/v1/users/:id/profile", userProfileHandler)
r.GET("/organizations/:org/teams/:team/members", membersHandler)

Example:

  • /api/v1/users/123/profileid="123"
  • /organizations/acme/teams/engineering/membersorg="acme", team="engineering"

Wildcard Routes

Wildcard routes capture the rest of the path using *param:

// Serve files from any path under /files/
r.GET("/files/*filepath", func(c *router.Context) {
    filepath := c.Param("filepath")
    c.JSON(200, map[string]string{"filepath": filepath})
})

Matches:

  • /files/images/logo.pngfilepath="images/logo.png"
  • /files/docs/api/v1/index.htmlfilepath="docs/api/v1/index.html"
  • /files/a/b/c/d/e/f.txtfilepath="a/b/c/d/e/f.txt"

Important:

  • Wildcards match everything after their position, including slashes
  • Only one wildcard per route
  • Wildcard must be the last segment
// ✅ Valid
r.GET("/static/*filepath", handler)
r.GET("/api/v1/files/*path", handler)

// ❌ Invalid - wildcard must be last
r.GET("/files/*path/metadata", handler) // Won't work

// ❌ Invalid - only one wildcard
r.GET("/files/*path1/other/*path2", handler) // Won't work

Route Matching Priority

When multiple routes could match a request, the router follows this priority order:

1. Static Routes (Highest Priority)

Exact matches are evaluated first:

r.GET("/users/me", currentUserHandler)      // Static
r.GET("/users/:id", getUserHandler)         // Parameter

Request: GET /users/me

  • ✅ Matches /users/me (static) - Selected
  • ❌ Could match /users/:id but static wins

2. Parameter Routes

After static routes, parameter routes are checked:

r.GET("/posts/:id", getPostHandler)
r.GET("/posts/*filepath", catchAllHandler)

Request: GET /posts/123

  • ❌ No static match
  • ✅ Matches /posts/:id - Selected
  • ❌ Could match /posts/*filepath but parameter wins

3. Wildcard Routes (Lowest Priority)

Wildcards are the catch-all:

r.GET("/files/*filepath", serveFileHandler)

Request: GET /files/images/logo.png

  • ❌ No static match
  • ❌ No parameter match
  • ✅ Matches /files/*filepath - Selected

Priority Examples

func main() {
    r := router.MustNew()
    
    // Priority 1: Static
    r.GET("/users/me", func(c *router.Context) {
        c.String(200, "Current user")
    })
    
    // Priority 2: Parameter
    r.GET("/users/:id", func(c *router.Context) {
        c.String(200, "User: "+c.Param("id"))
    })
    
    // Priority 3: Wildcard
    r.GET("/users/*path", func(c *router.Context) {
        c.String(200, "Catch-all: "+c.Param("path"))
    })
    
    http.ListenAndServe(":8080", r)
}

Tests:

curl http://localhost:8080/users/me
# Output: "Current user" (static route)

curl http://localhost:8080/users/123
# Output: "User: 123" (parameter route)

curl http://localhost:8080/users/123/posts
# Output: "Catch-all: 123/posts" (wildcard route)

Parameter Design Best Practices

The router optimizes parameter storage for routes with ≤8 parameters using fast array-based storage. Routes with >8 parameters fall back to map-based storage.

Optimization Threshold

  • ≤8 parameters: Array-based storage (fastest, zero allocations)
  • >8 parameters: Map-based storage (one allocation per request)

Best Practices

1. Keep Parameter Count ≤8

// ✅ GOOD: 2 parameters
r.GET("/users/:id/posts/:post_id", handler)

// ✅ GOOD: 4 parameters
r.GET("/api/:version/users/:id/posts/:post_id/comments/:comment_id", handler)

// ⚠️ WARNING: 9 parameters (requires map allocation)
r.GET("/a/:p1/b/:p2/c/:p3/d/:p4/e/:p5/f/:p6/g/:p7/h/:p8/i/:p9", handler)

2. Use Query Parameters for Additional Data

Instead of many path parameters, use query parameters:

// ❌ BAD: Too many path parameters
r.GET("/search/:category/:subcategory/:type/:status/:sort/:order/:page/:limit", handler)

// ✅ GOOD: Use query parameters for filters
r.GET("/search/:category", handler)
// Query: ?subcategory=electronics&type=product&status=active&sort=price&order=asc&page=1&limit=20

3. Use Request Body for Complex Data

For complex operations, use the request body:

// ❌ BAD: Many path parameters
r.POST("/api/:version/:resource/:action/:target/:scope/:context/:mode/:format", handler)

// ✅ GOOD: Use request body
r.POST("/api/v1/operations", handler)
// Body: {"resource": "...", "action": "...", "target": "...", ...}

4. Restructure Routes

Flatten hierarchies or consolidate parameters:

// ❌ BAD: 10 parameters in path
r.GET("/:a/:b/:c/:d/:e/:f/:g/:h/:i/:j", handler)

// ✅ GOOD: Flatten hierarchy or use query parameters
r.GET("/items", handler) // Use query: ?a=...&b=...&c=...

Runtime Warnings

The router automatically logs a warning when registering routes with >8 parameters:

WARN: route has more than 8 parameters, using map storage instead of fast array
  method=GET
  path=/api/:v1/:r1/:r2/:r3/:r4/:r5/:r6/:r7/:r8/:r9
  param_count=9
  recommendation=consider restructuring route to use query parameters or request body for additional data

When >8 Parameters Are Acceptable

  • Low-frequency endpoints (<100 req/s)
  • Legacy API compatibility requirements
  • Complex hierarchical resource structures that can’t be flattened

Performance Impact

  • ≤8 params: ~119ns/op, 0 allocations
  • >8 params: ~119ns/op, 1 allocation (~24 bytes)
  • Real-world impact: Negligible for most applications (<1% overhead)

Route Constraints

Add validation to parameters with constraints:

// Integer constraint
r.GET("/users/:id", getUserHandler).WhereInt("id")

// UUID constraint
r.GET("/entities/:uuid", getEntityHandler).WhereUUID("uuid")

// Custom regex
r.GET("/files/:filename", getFileHandler).WhereRegex("filename", `[a-zA-Z0-9.-]+`)

// Enum constraint
r.GET("/status/:state", getStatusHandler).WhereEnum("state", "active", "pending", "deleted")

Learn more: See the Route Constraints reference for all available constraints.

Common Patterns

RESTful Resources

// Standard REST endpoints
r.GET("/users", listUsers)              // List all
r.POST("/users", createUser)            // Create new
r.GET("/users/:id", getUser)            // Get one
r.PUT("/users/:id", updateUser)         // Update (full)
r.PATCH("/users/:id", patchUser)        // Update (partial)
r.DELETE("/users/:id", deleteUser)      // Delete

Nested Resources

// Comments belong to posts
r.GET("/posts/:post_id/comments", listComments)
r.POST("/posts/:post_id/comments", createComment)
r.GET("/posts/:post_id/comments/:id", getComment)
r.PUT("/posts/:post_id/comments/:id", updateComment)
r.DELETE("/posts/:post_id/comments/:id", deleteComment)

Action Routes

// Actions on resources
r.POST("/users/:id/activate", activateUser)
r.POST("/users/:id/deactivate", deactivateUser)
r.POST("/posts/:id/publish", publishPost)
r.POST("/orders/:id/cancel", cancelOrder)

File Serving

// Static file serving
r.GET("/assets/*filepath", serveAssets)
r.GET("/downloads/*filepath", serveDownloads)

func serveAssets(c *router.Context) {
    filepath := c.Param("filepath")
    c.ServeFile("./public/" + filepath)
}

Anti-Patterns

Avoid Ambiguous Routes

// ❌ BAD: Ambiguous - which route matches /users/delete?
r.GET("/users/:id", getUser)
r.DELETE("/users/:action", performAction)

// ✅ GOOD: Clear distinction
r.GET("/users/:id", getUser)
r.POST("/users/:id/actions/:action", performAction)

Avoid Overly Deep Hierarchies

// ❌ BAD: Too deep
r.GET("/api/v1/organizations/:org/teams/:team/projects/:proj/tasks/:task/comments/:id", handler)

// ✅ GOOD: Flatten or use query parameters
r.GET("/api/v1/comments/:id", handler) // Include org/team/proj/task in query or auth context

Next Steps

4 - Route Groups

Organize routes with groups, shared prefixes, and group-specific middleware.

Route groups help organize related routes. They share a common prefix. They can apply middleware to specific sets of routes.

Basic Groups

Create a group with a common prefix:

func main() {
    r := router.MustNew()
    r.Use(Logger()) // Global middleware
    
    // API v1 group
    v1 := r.Group("/api/v1")
    v1.GET("/users", listUsersV1)
    v1.POST("/users", createUserV1)
    v1.GET("/users/:id", getUserV1)
    
    http.ListenAndServe(":8080", r)
}

Routes created:

  • GET /api/v1/users
  • POST /api/v1/users
  • GET /api/v1/users/:id

Group-Specific Middleware

Apply middleware that only affects routes in the group:

func main() {
    r := router.MustNew()
    r.Use(Logger()) // Global - applies to all routes
    
    // Public API - no auth required
    public := r.Group("/api/public")
    public.GET("/health", healthHandler)
    public.GET("/version", versionHandler)
    
    // Private API - auth required
    private := r.Group("/api/private")
    private.Use(AuthRequired()) // Group middleware
    private.GET("/profile", profileHandler)
    private.POST("/settings", updateSettingsHandler)
    
    http.ListenAndServe(":8080", r)
}

func AuthRequired() router.HandlerFunc {
    return func(c *router.Context) {
        token := c.Request.Header.Get("Authorization")
        if token == "" {
            c.JSON(401, map[string]string{"error": "Unauthorized"})
            return
        }
        c.Next()
    }
}

Middleware execution:

  • /api/public/health → Logger only.
  • /api/private/profile → Logger + AuthRequired.

Nested Groups

Groups can be nested for hierarchical organization:

func main() {
    r := router.MustNew()
    r.Use(Logger())
    
    api := r.Group("/api")
    {
        v1 := api.Group("/v1")
        v1.Use(RateLimitV1()) // V1-specific rate limiting
        {
            // User endpoints
            users := v1.Group("/users")
            users.Use(UserAuth())
            {
                users.GET("/", listUsers)          // GET /api/v1/users/
                users.POST("/", createUser)        // POST /api/v1/users/
                users.GET("/:id", getUser)         // GET /api/v1/users/:id
                users.PUT("/:id", updateUser)      // PUT /api/v1/users/:id
                users.DELETE("/:id", deleteUser)   // DELETE /api/v1/users/:id
            }
            
            // Admin endpoints
            admin := v1.Group("/admin")
            admin.Use(AdminAuth())
            {
                admin.GET("/stats", getStats)                    // GET /api/v1/admin/stats
                admin.DELETE("/users/:id", adminDeleteUser)      // DELETE /api/v1/admin/users/:id
            }
        }
        
        v2 := api.Group("/v2")
        v2.Use(RateLimitV2()) // V2-specific rate limiting
        {
            v2.GET("/users", listUsersV2)
            v2.POST("/users", createUsersV2)
        }
    }
    
    http.ListenAndServe(":8080", r)
}

Routes created:

GET    /api/v1/users/
POST   /api/v1/users/
GET    /api/v1/users/:id
PUT    /api/v1/users/:id
DELETE /api/v1/users/:id
GET    /api/v1/admin/stats
DELETE /api/v1/admin/users/:id
GET    /api/v2/users
POST   /api/v2/users

Middleware Execution Order

For nested groups, middleware executes from outer to inner:

r.Use(GlobalMiddleware())                   // 1st
api := r.Group("/api", APIMiddleware())     // 2nd
v1 := api.Group("/v1", V1Middleware())      // 3rd
users := v1.Group("/users", UsersMiddleware()) // 4th
users.GET("/:id", RouteMiddleware(), handler)  // 5th → handler

// Execution order:
// GlobalMiddleware → APIMiddleware → V1Middleware → UsersMiddleware → RouteMiddleware → handler

Example with logging:

func main() {
    r := router.MustNew()
    
    r.Use(func(c *router.Context) {
        fmt.Println("1. Global middleware")
        c.Next()
    })
    
    api := r.Group("/api")
    api.Use(func(c *router.Context) {
        fmt.Println("2. API middleware")
        c.Next()
    })
    
    v1 := api.Group("/v1")
    v1.Use(func(c *router.Context) {
        fmt.Println("3. V1 middleware")
        c.Next()
    })
    
    v1.GET("/test", func(c *router.Context) {
        fmt.Println("4. Handler")
        c.String(200, "OK")
    })
    
    http.ListenAndServe(":8080", r)
}

Request to /api/v1/test prints:

1. Global middleware
2. API middleware
3. V1 middleware
4. Handler

Composing Group Middleware

Create reusable middleware bundles:

// Middleware bundles
func PublicAPI() []router.HandlerFunc {
    return []router.HandlerFunc{
        CORS(),
        RateLimit(1000),
    }
}

func AuthenticatedAPI() []router.HandlerFunc {
    return []router.HandlerFunc{
        CORS(),
        RateLimit(100),
        AuthRequired(),
    }
}

func AdminAPI() []router.HandlerFunc {
    return []router.HandlerFunc{
        CORS(),
        RateLimit(50),
        AuthRequired(),
        AdminOnly(),
    }
}

func main() {
    r := router.MustNew()
    r.Use(Logger(), Recovery())
    
    // Public endpoints
    public := r.Group("/api/public")
    public.Use(PublicAPI()...)
    public.GET("/status", statusHandler)
    
    // User endpoints
    user := r.Group("/api/user")
    user.Use(AuthenticatedAPI()...)
    user.GET("/profile", profileHandler)
    
    // Admin endpoints
    admin := r.Group("/api/admin")
    admin.Use(AdminAPI()...)
    admin.GET("/users", listUsersAdmin)
    
    http.ListenAndServe(":8080", r)
}

Organizing by Resource

Structure your API around resources:

func main() {
    r := router.MustNew()
    r.Use(Logger(), Recovery())
    
    // Setup route groups
    setupUserRoutes(r)
    setupPostRoutes(r)
    setupCommentRoutes(r)
    
    http.ListenAndServe(":8080", r)
}

func setupUserRoutes(r *router.Router) {
    users := r.Group("/api/users")
    users.Use(JSONContentType())
    
    users.GET("/", listUsers)
    users.POST("/", createUser)
    users.GET("/:id", getUser)
    users.PUT("/:id", updateUser)
    users.DELETE("/:id", deleteUser)
}

func setupPostRoutes(r *router.Router) {
    posts := r.Group("/api/posts")
    posts.Use(JSONContentType())
    
    posts.GET("/", listPosts)
    posts.POST("/", AuthRequired(), createPost)
    posts.GET("/:id", getPost)
    posts.PUT("/:id", AuthRequired(), updatePost)
    posts.DELETE("/:id", AuthRequired(), deletePost)
}

func setupCommentRoutes(r *router.Router) {
    comments := r.Group("/api/comments")
    comments.Use(JSONContentType())
    
    comments.GET("/", listComments)
    comments.POST("/", AuthRequired(), createComment)
    comments.GET("/:id", getComment)
    comments.PUT("/:id", AuthRequired(), updateComment)
    comments.DELETE("/:id", AuthRequired(), deleteComment)
}

Versioning with Groups

Organize API versions:

func main() {
    r := router.MustNew()
    r.Use(Logger())
    
    // Version 1 - Stable API
    v1 := r.Group("/api/v1")
    v1.Use(JSONContentType())
    {
        v1.GET("/users", listUsersV1)
        v1.GET("/users/:id", getUserV1)
        v1.GET("/posts", listPostsV1)
    }
    
    // Version 2 - New features
    v2 := r.Group("/api/v2")
    v2.Use(JSONContentType())
    {
        v2.GET("/users", listUsersV2)        // Enhanced user list
        v2.GET("/users/:id", getUserV2)      // Additional fields
        v2.GET("/posts", listPostsV2)        // Pagination support
        v2.GET("/posts/:id/likes", getPostLikesV2) // New endpoint
    }
    
    // Beta features
    beta := r.Group("/api/beta")
    beta.Use(JSONContentType(), BetaWarning())
    {
        beta.GET("/experimental", experimentalFeature)
    }
    
    http.ListenAndServe(":8080", r)
}

Group Configuration Patterns

Pattern 1: Inline Configuration

api := r.Group("/api")
api.Use(Logger(), Auth())
api.GET("/users", handler)
api.POST("/users", handler)

Pattern 2: Block Scope

api := r.Group("/api")
{
    api.Use(Logger(), Auth())
    api.GET("/users", handler)
    api.POST("/users", handler)
}

Pattern 3: Function-Based Setup

setupAPIRoutes := func(parent *router.Group) {
    api := parent.Group("/api")
    api.Use(Logger(), Auth())
    api.GET("/users", handler)
    api.POST("/users", handler)
}

setupAPIRoutes(r)

Best Practices

// ✅ GOOD: Related routes grouped
users := r.Group("/api/users")
users.GET("/", listUsers)
users.POST("/", createUser)
users.GET("/:id", getUser)

// ❌ BAD: Scattered registration
r.GET("/api/users", listUsers)
r.GET("/api/posts", listPosts)
r.POST("/api/users", createUser)

2. Apply Middleware at the Right Level

// ✅ GOOD: Auth only where needed
public := r.Group("/api/public")
public.GET("/status", statusHandler)

private := r.Group("/api/private")
private.Use(AuthRequired())
private.GET("/profile", profileHandler)

// ❌ BAD: Auth on everything
r.Use(AuthRequired()) // Public endpoints won't work!
r.GET("/api/status", statusHandler)

3. Use Descriptive Names

// ✅ GOOD: Clear purpose
adminAPI := r.Group("/admin")
userAPI := r.Group("/user")
publicAPI := r.Group("/public")

// ❌ BAD: Unclear
g1 := r.Group("/api")
g2 := r.Group("/routes")
group := r.Group("/stuff")

4. Keep Nesting Shallow

// ✅ GOOD: 2-3 levels
api := r.Group("/api")
v1 := api.Group("/v1")
v1.GET("/users", handler)

// ⚠️ OKAY: 4 levels (limit)
api := r.Group("/api")
v1 := api.Group("/v1")
users := v1.Group("/users")
users.GET("/:id", handler)

// ❌ BAD: Too deep (5+ levels)
api := r.Group("/api")
v1 := api.Group("/v1")
orgs := v1.Group("/orgs")
teams := orgs.Group("/:org/teams")
projects := teams.Group("/:team/projects")
projects.GET("/", handler) // /api/v1/orgs/:org/teams/:team/projects/

Complete Example

package main

import (
    "fmt"
    "net/http"
    "rivaas.dev/router"
)

func main() {
    r := router.MustNew()
    
    // Global middleware
    r.Use(Logger(), Recovery())
    
    // Public routes (no auth)
    public := r.Group("/api/public")
    public.Use(CORS())
    {
        public.GET("/health", healthHandler)
        public.GET("/version", versionHandler)
    }
    
    // API v1
    v1 := r.Group("/api/v1")
    v1.Use(CORS(), JSONContentType())
    {
        // User routes (auth required)
        users := v1.Group("/users")
        users.Use(AuthRequired())
        {
            users.GET("/", listUsers)
            users.POST("/", createUser)
            users.GET("/:id", getUser)
            users.PUT("/:id", updateUser)
            users.DELETE("/:id", deleteUser)
        }
        
        // Admin routes (admin auth required)
        admin := v1.Group("/admin")
        admin.Use(AuthRequired(), AdminOnly())
        {
            admin.GET("/stats", adminStats)
            admin.GET("/users", adminListUsers)
        }
    }
    
    fmt.Println("Server starting on :8080")
    http.ListenAndServe(":8080", r)
}

// Middleware
func Logger() router.HandlerFunc {
    return func(c *router.Context) {
        fmt.Printf("[%s] %s\n", c.Request.Method, c.Request.URL.Path)
        c.Next()
    }
}

func Recovery() router.HandlerFunc {
    return func(c *router.Context) {
        defer func() {
            if err := recover(); err != nil {
                c.JSON(500, map[string]string{"error": "Internal server error"})
            }
        }()
        c.Next()
    }
}

func CORS() router.HandlerFunc {
    return func(c *router.Context) {
        c.Header("Access-Control-Allow-Origin", "*")
        c.Next()
    }
}

func JSONContentType() router.HandlerFunc {
    return func(c *router.Context) {
        c.Header("Content-Type", "application/json")
        c.Next()
    }
}

func AuthRequired() router.HandlerFunc {
    return func(c *router.Context) {
        token := c.Request.Header.Get("Authorization")
        if token == "" {
            c.JSON(401, map[string]string{"error": "Unauthorized"})
            return
        }
        c.Next()
    }
}

func AdminOnly() router.HandlerFunc {
    return func(c *router.Context) {
        // Check if user is admin...
        c.Next()
    }
}

// Handlers (simplified)
func healthHandler(c *router.Context) { c.String(200, "OK") }
func versionHandler(c *router.Context) { c.String(200, "v1.0.0") }
func listUsers(c *router.Context) { c.JSON(200, []string{"user1", "user2"}) }
func createUser(c *router.Context) { c.JSON(201, map[string]string{"id": "1"}) }
func getUser(c *router.Context) { c.JSON(200, map[string]string{"id": c.Param("id")}) }
func updateUser(c *router.Context) { c.JSON(200, map[string]string{"id": c.Param("id")}) }
func deleteUser(c *router.Context) { c.Status(204) }
func adminStats(c *router.Context) { c.JSON(200, map[string]int{"users": 100}) }
func adminListUsers(c *router.Context) { c.JSON(200, []string{"all", "users"}) }

Next Steps

5 - Middleware

Add cross-cutting concerns like logging, authentication, and error handling with middleware.

Middleware functions execute before route handlers. They perform cross-cutting concerns like authentication, logging, and rate limiting.

Basic Usage

Middleware is a function that wraps your handlers:

func Logger() router.HandlerFunc {
    return func(c *router.Context) {
        start := time.Now()
        path := c.Request.URL.Path
        
        c.Next() // Continue to next handler
        
        duration := time.Since(start)
        fmt.Printf("[%s] %s - %v\n", c.Request.Method, path, duration)
    }
}

func main() {
    r := router.MustNew()
    
    // Apply middleware globally
    r.Use(Logger())
    
    r.GET("/", handler)
    http.ListenAndServe(":8080", r)
}

Key concepts:

  • c.Next() - Continues to the next middleware or handler.
  • Call c.Next() to proceed. Don’t call it to stop the chain.
  • Middleware runs in registration order.

Middleware Scope

Global Middleware

Applied to all routes:

r := router.MustNew()

// These apply to ALL routes
r.Use(Logger())
r.Use(Recovery())
r.Use(CORS())

r.GET("/", handler)
r.GET("/users", usersHandler)

Group Middleware

Applied only to routes in a group:

r := router.MustNew()
r.Use(Logger()) // Global

// Public routes - no auth
public := r.Group("/api/public")
public.GET("/status", statusHandler)

// Private routes - auth required
private := r.Group("/api/private")
private.Use(AuthRequired()) // Group-level
private.GET("/profile", profileHandler)

Route-Specific Middleware

Applied to individual routes:

r := router.MustNew()
r.Use(Logger()) // Global

// Auth only for this route
r.GET("/admin", AdminAuth(), adminHandler)

// Multiple middleware for one route
r.POST("/upload", RateLimit(), ValidateFile(), uploadHandler)

Built-in Middleware

The router includes production-ready middleware in sub-packages. See the Middleware Reference for complete options.

Security

Security Headers

import "rivaas.dev/router/middleware/security"

r.Use(security.New(
    security.WithHSTS(true),
    security.WithFrameDeny(true),
    security.WithContentTypeNosniff(true),
))

CORS

import "rivaas.dev/router/middleware/cors"

r.Use(cors.New(
    cors.WithAllowedOrigins("https://example.com"),
    cors.WithAllowedMethods("GET", "POST", "PUT", "DELETE"),
    cors.WithAllowedHeaders("Content-Type", "Authorization"),
    cors.WithAllowCredentials(true),
))

Basic Auth

import "rivaas.dev/router/middleware/basicauth"

admin := r.Group("/admin")
admin.Use(basicauth.New(
    basicauth.WithCredentials("admin", "secret"),
))

Observability

Access Log

import (
    "log/slog"
    "rivaas.dev/router/middleware/accesslog"
)

logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
r.Use(accesslog.New(
    accesslog.WithLogger(logger),
    accesslog.WithExcludePaths("/health", "/metrics"),
    accesslog.WithSlowThreshold(500 * time.Millisecond),
))

Request ID

import "rivaas.dev/router/middleware/requestid"

// UUID v7 by default (36 chars, time-ordered, RFC 9562)
r.Use(requestid.New())

// Use ULID for shorter IDs (26 chars)
r.Use(requestid.New(requestid.WithULID()))

// Custom header name
r.Use(requestid.New(requestid.WithHeader("X-Correlation-ID")))

// Later in handlers:
func handler(c *router.Context) {
    id := requestid.Get(c)
    fmt.Println("Request ID:", id)
}

Reliability

Recovery

import "rivaas.dev/router/middleware/recovery"

r.Use(recovery.New(
    recovery.WithPrintStack(true),
    recovery.WithLogger(logger),
))

Timeout

import "rivaas.dev/router/middleware/timeout"

r.Use(timeout.New(
    timeout.WithDuration(30 * time.Second),
    timeout.WithMessage("Request timeout"),
))

Rate Limit

import "rivaas.dev/router/middleware/ratelimit"

r.Use(ratelimit.New(
    ratelimit.WithRequestsPerSecond(1000),
    ratelimit.WithBurst(100),
    ratelimit.WithKeyFunc(func(c *router.Context) string {
        return c.ClientIP() // Rate limit by IP
    }),
))

Body Limit

import "rivaas.dev/router/middleware/bodylimit"

r.Use(bodylimit.New(
    bodylimit.WithLimit(10 * 1024 * 1024), // 10MB
))

Performance

Compression

import "rivaas.dev/router/middleware/compression"

r.Use(compression.New(
    compression.WithLevel(compression.DefaultCompression),
    compression.WithMinSize(1024), // Don't compress <1KB
))

Middleware Ordering

The order in which middleware is applied matters. Recommended order:

r := router.MustNew()

// 1. Request ID - Generate early for logging/tracing
r.Use(requestid.New())

// 2. AccessLog - Log all requests including failed ones
r.Use(accesslog.New())

// 3. Recovery - Catch panics from all other middleware
r.Use(recovery.New())

// 4. Security/CORS - Set security headers early
r.Use(security.New())
r.Use(cors.New())

// 5. Body Limit - Reject large requests before processing
r.Use(bodylimit.New())

// 6. Rate Limit - Reject excessive requests before processing
r.Use(ratelimit.New())

// 7. Timeout - Set time limits for downstream processing
r.Use(timeout.New())

// 8. Authentication - Verify identity after rate limiting
r.Use(auth.New())

// 9. Compression - Compress responses (last)
r.Use(compression.New())

// 10. Your application routes
r.GET("/", handler)

Why this order?

  1. RequestID first - Generates a unique ID that other middleware can use
  2. Logger early - Captures all activity including errors
  3. Recovery early - Catches panics to prevent crashes
  4. Security/CORS - Applies security policies before business logic
  5. BodyLimit - Prevents reading excessive request bodies (DoS protection)
  6. RateLimit - Blocks excessive requests before expensive operations
  7. Timeout - Sets deadlines for request processing
  8. Auth - Authenticates after rate limiting but before business logic
  9. Compression - Compresses response bodies (should be last)

Writing Custom Middleware

Basic Middleware Pattern

func MyMiddleware() router.HandlerFunc {
    return func(c *router.Context) {
        // Before request processing
        fmt.Println("Before handler")
        
        c.Next() // Execute next middleware/handler
        
        // After request processing
        fmt.Println("After handler")
    }
}

Middleware with Configuration

func RateLimit(requestsPerSecond int) router.HandlerFunc {
    // Setup (runs once when middleware is created)
    limiter := rate.NewLimiter(rate.Limit(requestsPerSecond), requestsPerSecond)
    
    return func(c *router.Context) {
        // Per-request logic
        if !limiter.Allow() {
            c.JSON(429, map[string]string{
                "error": "Too many requests",
            })
            return // Don't call c.Next() - stop the chain
        }
        c.Next()
    }
}

// Usage
r.Use(RateLimit(100)) // 100 requests per second

Middleware with Dependencies

func Auth(db *Database) router.HandlerFunc {
    return func(c *router.Context) {
        token := c.Request.Header.Get("Authorization")
        
        user, err := db.ValidateToken(token)
        if err != nil {
            c.JSON(401, map[string]string{
                "error": "Unauthorized",
            })
            return
        }
        
        // Store user in request context for handlers
        ctx := context.WithValue(c.Request.Context(), "user", user)
        c.Request = c.Request.WithContext(ctx)
        c.Next()
    }
}

// Usage
db := NewDatabase()
r.Use(Auth(db))

Conditional Middleware

func ConditionalAuth() router.HandlerFunc {
    return func(c *router.Context) {
        // Skip auth for public endpoints
        if c.Request.URL.Path == "/public" {
            c.Next()
            return
        }
        
        // Require auth for other endpoints
        token := c.Request.Header.Get("Authorization")
        if token == "" {
            c.JSON(401, map[string]string{
                "error": "Unauthorized",
            })
            return
        }
        
        c.Next()
    }
}

Middleware Patterns

Pattern: Error Handling Middleware

func ErrorHandler() router.HandlerFunc {
    return func(c *router.Context) {
        defer func() {
            if err := recover(); err != nil {
                log.Printf("Panic: %v", err)
                c.JSON(500, map[string]string{
                    "error": "Internal server error",
                })
            }
        }()
        
        c.Next()
    }
}

Pattern: Logging Middleware

func Logger() router.HandlerFunc {
    return func(c *router.Context) {
        start := time.Now()
        path := c.Request.URL.Path
        method := c.Request.Method
        
        c.Next()
        
        duration := time.Since(start)
        status := c.Writer.Status()
        
        log.Printf("[%s] %s %s - %d (%v)",
            method,
            path,
            c.ClientIP(),
            status,
            duration,
        )
    }
}

Pattern: Authentication Middleware

func JWTAuth(secret string) router.HandlerFunc {
    return func(c *router.Context) {
        authHeader := c.Request.Header.Get("Authorization")
        if authHeader == "" {
            c.JSON(401, map[string]string{
                "error": "Missing authorization header",
            })
            return
        }
        
        // Extract token (Bearer <token>)
        parts := strings.SplitN(authHeader, " ", 2)
        if len(parts) != 2 || parts[0] != "Bearer" {
            c.JSON(401, map[string]string{
                "error": "Invalid authorization header format",
            })
            return
        }
        
        token := parts[1]
        claims, err := validateJWT(token, secret)
        if err != nil {
            c.JSON(401, map[string]string{
                "error": "Invalid token",
            })
            return
        }
        
        // Store claims in request context
        ctx := c.Request.Context()
        ctx = context.WithValue(ctx, "user_id", claims.UserID)
        ctx = context.WithValue(ctx, "user_email", claims.Email)
        c.Request = c.Request.WithContext(ctx)
        c.Next()
    }
}

Pattern: Request ID Middleware

The built-in requestid middleware handles this pattern with UUID v7 or ULID:

import "rivaas.dev/router/middleware/requestid"

// UUID v7 (default) - time-ordered, 36 chars
r.Use(requestid.New())

// ULID - shorter, 26 chars
r.Use(requestid.New(requestid.WithULID()))

// Access in handlers
func handler(c *router.Context) {
    id := requestid.Get(c)  // Get from context
    // Or from header: c.Response.Header().Get("X-Request-ID")
}

If you need a custom implementation:

func RequestID() router.HandlerFunc {
    return func(c *router.Context) {
        // Check for existing request ID
        requestID := c.Request.Header.Get("X-Request-ID")
        if requestID == "" {
            // Generate new UUID v7
            requestID = uuid.Must(uuid.NewV7()).String()
        }
        
        // Store in request context and response header
        ctx := context.WithValue(c.Request.Context(), "request_id", requestID)
        c.Request = c.Request.WithContext(ctx)
        c.Header("X-Request-ID", requestID)
        
        c.Next()
    }
}

Best Practices

1. Always Call c.Next()

Unless you want to stop the middleware chain:

// ✅ GOOD: Calls c.Next() to continue
func Logger() router.HandlerFunc {
    return func(c *router.Context) {
        start := time.Now()
        c.Next() // Continue to handler
        duration := time.Since(start)
        log.Printf("Duration: %v", duration)
    }
}

// ✅ GOOD: Doesn't call c.Next() to stop chain
func Auth() router.HandlerFunc {
    return func(c *router.Context) {
        if !isAuthorized(c) {
            c.JSON(401, map[string]string{"error": "Unauthorized"})
            return // Don't call c.Next()
        }
        c.Next()
    }
}

2. Keep Middleware Focused

Each middleware should do one thing:

// ✅ GOOD: Single responsibility
func Logger() router.HandlerFunc { ... }
func Auth() router.HandlerFunc { ... }
func RateLimit() router.HandlerFunc { ... }

// ❌ BAD: Does too much
func SuperMiddleware() router.HandlerFunc {
    return func(c *router.Context) {
        // Logging
        // Auth
        // Rate limiting
        // ...
        c.Next()
    }
}

3. Use Functional Options for Configuration

type Config struct {
    Limit int
    Burst int
}

type Option func(*Config)

func WithLimit(limit int) Option {
    return func(c *Config) {
        c.Limit = limit
    }
}

func WithBurst(burst int) Option {
    return func(c *Config) {
        c.Burst = burst
    }
}

func RateLimit(opts ...Option) router.HandlerFunc {
    config := &Config{
        Limit: 100,
        Burst: 10,
    }
    for _, opt := range opts {
        opt(config)
    }
    
    limiter := rate.NewLimiter(rate.Limit(config.Limit), config.Burst)
    
    return func(c *router.Context) {
        if !limiter.Allow() {
            c.JSON(429, map[string]string{"error": "Too many requests"})
            return
        }
        c.Next()
    }
}

// Usage
r.Use(RateLimit(
    WithLimit(1000),
    WithBurst(100),
))

4. Handle Errors Gracefully

func Middleware() router.HandlerFunc {
    return func(c *router.Context) {
        if err := doSomething(c); err != nil {
            // Log error
            log.Printf("Middleware error: %v", err)
            
            // Return error response
            c.JSON(500, map[string]string{
                "error": "Internal server error",
            })
            return // Don't call c.Next()
        }
        c.Next()
    }
}

Complete Example

package main

import (
    "fmt"
    "log"
    "log/slog"
    "net/http"
    "os"
    "time"
    
    "rivaas.dev/router"
    "rivaas.dev/router/middleware/accesslog"
    "rivaas.dev/router/middleware/cors"
    "rivaas.dev/router/middleware/recovery"
    "rivaas.dev/router/middleware/requestid"
    "rivaas.dev/router/middleware/security"
)

func main() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    r := router.MustNew()
    
    // Global middleware (applies to all routes)
    r.Use(requestid.New())
    r.Use(accesslog.New(accesslog.WithLogger(logger)))
    r.Use(recovery.New())
    r.Use(security.New())
    r.Use(cors.New(
        cors.WithAllowedOrigins("*"),
        cors.WithAllowedMethods("GET", "POST", "PUT", "DELETE"),
    ))
    
    // Public routes
    r.GET("/health", healthHandler)
    r.GET("/public", publicHandler)
    
    // API routes with auth
    api := r.Group("/api")
    api.Use(JWTAuth("your-secret-key"))
    {
        api.GET("/profile", profileHandler)
        api.POST("/posts", createPostHandler)
        
        // Admin routes with additional middleware
        admin := api.Group("/admin")
        admin.Use(RequireAdmin())
        {
            admin.GET("/users", listUsersHandler)
            admin.DELETE("/users/:id", deleteUserHandler)
        }
    }
    
    log.Fatal(http.ListenAndServe(":8080", r))
}

// Custom middleware
func JWTAuth(secret string) router.HandlerFunc {
    return func(c *router.Context) {
        token := c.Request.Header.Get("Authorization")
        if token == "" {
            c.JSON(401, map[string]string{"error": "Unauthorized"})
            return
        }
        // Validate token...
        c.Next()
    }
}

func RequireAdmin() router.HandlerFunc {
    return func(c *router.Context) {
        // Check if user is admin...
        c.Next()
    }
}

// Handlers
func healthHandler(c *router.Context) {
    c.JSON(200, map[string]string{"status": "OK"})
}

func publicHandler(c *router.Context) {
    c.JSON(200, map[string]string{"message": "Public endpoint"})
}

func profileHandler(c *router.Context) {
    c.JSON(200, map[string]string{"user": "john@example.com"})
}

func createPostHandler(c *router.Context) {
    c.JSON(201, map[string]string{"message": "Post created"})
}

func listUsersHandler(c *router.Context) {
    c.JSON(200, []string{"user1", "user2"})
}

func deleteUserHandler(c *router.Context) {
    c.Status(204)
}

Next Steps

6 - Context

Understand the Context API, request/response methods, and critical memory safety rules.

The router.Context provides access to the request/response and various utility methods. Understanding its lifecycle is critical for memory safety.

⚠️ Memory Safety - CRITICAL

Context objects are pooled and reused across requests. You must understand context lifecycle for memory safety.

CRITICAL RULES

  1. DO NOT retain references to Context objects beyond the request handler lifetime.
  2. For async operations, copy needed data from Context before starting goroutines.
  3. The router automatically returns contexts to the pool after request completion.
  4. DO NOT access Context concurrently. It is NOT thread-safe.

Why This Matters

  • Memory leaks: Retaining references prevents contexts from being garbage collected.
  • Data corruption: Contexts are reused. Old data may appear in new requests.
  • Security issues: Sensitive request data may leak to other requests.
  • Undefined behavior: Use-after-release causes unpredictable bugs.

Correct Usage

// ✅ CORRECT: Normal handler - context used within handler
func handler(c *router.Context) {
    userID := c.Param("id")
    c.JSON(200, map[string]string{"id": userID})
    // Context automatically returned to pool by router
}

// ✅ CORRECT: Async operation with copied data
func handler(c *router.Context) {
    // Copy needed data before starting goroutine
    userID := c.Param("id")
    go func(id string) {
        // Process async work with copied data...
        processAsync(id)
    }(userID)
}

Incorrect Usage

// ❌ WRONG: Retaining context reference
var globalContext *router.Context

func handler(c *router.Context) {
    globalContext = c // BAD! Memory leak and data corruption
}

// ❌ WRONG: Passing context to goroutine
func handler(c *router.Context) {
    go func(ctx *router.Context) {
        // BAD! Context may be reused by another request
        processAsync(ctx.Param("id"))
    }(c)
}

// ❌ WRONG: Storing context in struct
type Service struct {
    ctx *router.Context // BAD! Never do this
}

Request Information

Basic Request Data

func handler(c *router.Context) {
    // HTTP method
    method := c.Request.Method // "GET", "POST", etc.
    
    // URL path
    path := c.Request.URL.Path // "/users/123"
    
    // Headers
    userAgent := c.Request.Header.Get("User-Agent")
    contentType := c.Request.Header.Get("Content-Type")
    
    // Remote address
    remoteAddr := c.Request.RemoteAddr // "192.168.1.1:12345"
}

Path Parameters

Extract parameters from the URL path:

// Route: /users/:id/posts/:post_id
r.GET("/users/:id/posts/:post_id", func(c *router.Context) {
    userID := c.Param("id")
    postID := c.Param("post_id")
    
    c.JSON(200, map[string]string{
        "user_id": userID,
        "post_id": postID,
    })
})

Query Parameters

// GET /search?q=golang&limit=10&page=2
r.GET("/search", func(c *router.Context) {
    query := c.Query("q")        // "golang"
    limit := c.Query("limit")    // "10"
    page := c.Query("page")      // "2"
    
    c.JSON(200, map[string]string{
        "query": query,
        "limit": limit,
        "page":  page,
    })
})

Form Data

// POST with form data
r.POST("/login", func(c *router.Context) {
    username := c.FormValue("username")
    password := c.FormValue("password")
    
    c.JSON(200, map[string]string{
        "username": username,
    })
})

Response Methods

JSON Responses

// Standard JSON (HTML-escaped)
c.JSON(200, data)

// Indented JSON (for debugging)
c.IndentedJSON(200, data)

// Pure JSON (no HTML escaping - 35% faster!)
c.PureJSON(200, data)

// Secure JSON (anti-hijacking prefix)
c.SecureJSON(200, data)

// ASCII JSON (pure ASCII with \uXXXX)
c.AsciiJSON(200, data)

// JSONP (with callback)
c.JSONP(200, data, "callback")

Other Response Formats

// YAML
c.YAML(200, config)

// Plain text
c.String(200, "Hello, World!")
c.Stringf(200, "Hello, %s!", name)

// HTML
c.HTML(200, "<h1>Welcome</h1>")

// Binary data
c.Data(200, "image/png", imageBytes)

// Stream from reader (zero-copy!)
c.DataFromReader(200, size, "video/mp4", file, nil)

// Status only
c.Status(204) // No Content

File Serving

// Serve file
c.ServeFile("/path/to/file.pdf")

// Force download
c.Download("/path/to/file.pdf", "custom-name.pdf")

Request Headers

Reading Headers

func handler(c *router.Context) {
    userAgent := c.Request.Header.Get("User-Agent")
    auth := c.Request.Header.Get("Authorization")
    contentType := c.Request.Header.Get("Content-Type")
}

Setting Response Headers

func handler(c *router.Context) {
    c.Header("Cache-Control", "no-cache")
    c.Header("X-Custom-Header", "value")
    c.JSON(200, data)
}

Helper Methods

Content Type Detection

func handler(c *router.Context) {
    if c.IsJSON() {
        // Request has JSON content-type
    }
    
    if c.AcceptsJSON() {
        c.JSON(200, data)
    } else if c.AcceptsHTML() {
        c.HTML(200, htmlContent)
    }
}

Client Information

func handler(c *router.Context) {
    clientIP := c.ClientIP()       // Real IP (considers X-Forwarded-For)
    isSecure := c.IsHTTPS()       // HTTPS check
}

Redirects

func handler(c *router.Context) {
    c.Redirect(301, "/new-url") // Permanent redirect
    c.Redirect(302, "/temp")    // Temporary redirect
}

Cookies

// Set cookie
c.SetCookie(
    "session_id",    // name
    "abc123",        // value
    3600,            // max age (seconds)
    "/",             // path
    "",              // domain
    false,           // secure
    true,            // httpOnly
)

// Get cookie
sessionID, err := c.GetCookie("session_id")

Passing Values Between Middleware

Use context.WithValue() to pass values between middleware and handlers:

// Define context keys to avoid collisions
type contextKey string
const userKey contextKey = "user"

// In middleware - create new request with value
func AuthMiddleware() router.HandlerFunc {
    return func(c *router.Context) {
        user := authenticateUser(c)
        
        // Create new context with value
        ctx := context.WithValue(c.Request.Context(), userKey, user)
        c.Request = c.Request.WithContext(ctx)
        
        c.Next()
    }
}

// In handler - retrieve value from request context
func handler(c *router.Context) {
    user, ok := c.Request.Context().Value(userKey).(*User)
    if !ok || user == nil {
        c.JSON(401, map[string]string{"error": "Unauthorized"})
        return
    }
    
    c.JSON(200, user)
}

File Uploads

r.POST("/upload", func(c *router.Context) {
    // Single file
    file, err := c.File("avatar")
    if err != nil {
        c.JSON(400, map[string]string{"error": "avatar required"})
        return
    }
    
    // File info
    fmt.Printf("Name: %s, Size: %d, Type: %s\n", 
        file.Name, file.Size, file.ContentType)
    
    // Save file
    if err := file.Save("./uploads/" + file.Name); err != nil {
        c.JSON(500, map[string]string{"error": "failed to save"})
        return
    }
    
    c.JSON(200, map[string]string{"filename": file.Name})
})

// Multiple files
r.POST("/upload-many", func(c *router.Context) {
    files, err := c.Files("documents")
    if err != nil {
        c.JSON(400, map[string]string{"error": "documents required"})
        return
    }
    
    for _, f := range files {
        f.Save("./uploads/" + f.Name)
    }
    
    c.JSON(200, map[string]int{"count": len(files)})
})

Performance Tips

Extract Data Immediately

// ✅ GOOD: Extract data early
func handler(c *router.Context) {
    userID := c.Param("id")
    query := c.Query("q")
    
    // Use extracted data
    result := processData(userID, query)
    c.JSON(200, result)
}

// ❌ BAD: Don't store context reference
var globalContext *router.Context
func handler(c *router.Context) {
    globalContext = c // Memory leak!
}

Choose the Right Response Method

// Use PureJSON for HTML content (35% faster than JSON)
c.PureJSON(200, dataWithHTMLStrings)

// Use Data() for binary (98% faster than JSON)
c.Data(200, "image/png", imageBytes)

// Avoid YAML in hot paths (9x slower than JSON)
// c.YAML(200, data) // Only for config/admin endpoints

Complete Example

package main

import (
    "encoding/json"
    "net/http"
    "rivaas.dev/router"
)

func main() {
    r := router.MustNew()
    
    // Request parameters
    r.GET("/users/:id", func(c *router.Context) {
        id := c.Param("id")
        c.JSON(200, map[string]string{"id": id})
    })
    
    // Query parameters
    r.GET("/search", func(c *router.Context) {
        q := c.Query("q")
        c.JSON(200, map[string]string{"query": q})
    })
    
    // Form data
    r.POST("/login", func(c *router.Context) {
        username := c.FormValue("username")
        c.JSON(200, map[string]string{"username": username})
    })
    
    // JSON request body
    r.POST("/users", func(c *router.Context) {
        var req struct {
            Name  string `json:"name"`
            Email string `json:"email"`
        }
        
        if err := json.NewDecoder(c.Request.Body).Decode(&req); err != nil {
            c.JSON(400, map[string]string{"error": "Invalid JSON"})
            return
        }
        
        c.JSON(201, req)
    })
    
    // Headers and cookies
    r.GET("/info", func(c *router.Context) {
        userAgent := c.Request.Header.Get("User-Agent")
        session, _ := c.GetCookie("session_id")
        
        c.Header("X-Custom", "value")
        c.JSON(200, map[string]string{
            "user_agent": userAgent,
            "session":    session,
        })
    })
    
    http.ListenAndServe(":8080", r)
}

Next Steps

7 - Request Binding

Parse and bind request data to Go structs.

Request binding parses request data (query parameters, URL parameters, form data, JSON) into Go structs.

Router Context Methods

The router Context provides basic binding and data access methods.

Strict JSON Binding

BindStrict() binds JSON with strict validation:

r.POST("/users", func(c *router.Context) {
    var req CreateUserRequest
    if err := c.BindStrict(&req, router.BindOptions{MaxBytes: 1 << 20}); err != nil {
        return // Error response already written
    }
    c.JSON(201, req)
})

Features:

  • Rejects unknown fields. Catches typos.
  • Enforces request body size limits.
  • Returns appropriate HTTP status codes. 400 for malformed. 422 for type errors.

Manual Parameter Access

For simple cases, access parameters directly:

// Query parameters
r.GET("/search", func(c *router.Context) {
    query := c.Query("q")
    limit := c.QueryDefault("limit", "10")
    
    c.JSON(200, map[string]string{
        "query": query,
        "limit": limit,
    })
})

// URL parameters
r.GET("/users/:id", func(c *router.Context) {
    userID := c.Param("id")
    c.JSON(200, map[string]string{"user_id": userID})
})

// Form data
r.POST("/login", func(c *router.Context) {
    username := c.FormValue("username")
    password := c.FormValue("password")
    // ...
})

Content Type Validation

Validate content type before binding:

r.POST("/users", func(c *router.Context) {
    if !c.RequireContentTypeJSON() {
        return // 415 Unsupported Media Type already sent
    }
    
    var req CreateUserRequest
    if err := c.BindStrict(&req, router.BindOptions{}); err != nil {
        return
    }
    c.JSON(201, req)
})

Streaming Large Payloads

For large arrays, stream instead of loading into memory:

// Stream JSON array items
r.POST("/bulk/users", func(c *router.Context) {
    err := router.StreamJSONArray(c, func(user User) error {
        return processUser(user)
    }, 10000) // Max 10k items
    
    if err != nil {
        return
    }
    c.NoContent()
})

// Stream NDJSON (newline-delimited JSON)
r.POST("/import", func(c *router.Context) {
    err := router.StreamNDJSON(c, func(item Record) error {
        return importRecord(item)
    })
    
    if err != nil {
        return
    }
    c.NoContent()
})

Binding Package (Full Features)

For comprehensive binding with struct tags, use the binding package:

import "rivaas.dev/binding"

// Bind query parameters to struct
type SearchRequest struct {
    Query string `query:"q"`
    Limit int    `query:"limit" default:"10"`
    Page  int    `query:"page" default:"1"`
}

r.GET("/search", func(c *router.Context) {
    var req SearchRequest
    if err := binding.Query(c.Request, &req); err != nil {
        c.JSON(400, map[string]string{"error": err.Error()})
        return
    }
    c.JSON(200, req)
})

Binding Methods (binding package)

binding.Query(r *http.Request, dst any) error    // Query parameters
binding.Params(params map[string]string, dst any) error  // URL parameters
binding.JSON(r *http.Request, dst any) error     // JSON body
binding.Form(r *http.Request, dst any) error     // Form data
binding.Headers(r *http.Request, dst any) error  // Request headers
binding.Cookies(r *http.Request, dst any) error  // Cookies

Supported Types

Primitives:

type Example struct {
    String  string  `query:"string"`
    Int     int     `query:"int"`
    Int64   int64   `query:"int64"`
    Float64 float64 `query:"float64"`
    Bool    bool    `query:"bool"`
}

Time and Duration:

type Example struct {
    Time     time.Time     `query:"time"`      // RFC3339, ISO8601, etc.
    Duration time.Duration `query:"duration"`  // "5m", "1h30m", etc.
}

Network Types:

type Example struct {
    IP     net.IP     `query:"ip"`      // "192.168.1.1"
    IPNet  net.IPNet  `query:"ipnet"`   // "192.168.1.0/24"
    URL    url.URL    `query:"url"`     // "https://example.com"
}

Slices:

type Example struct {
    Tags  []string `query:"tags"`   // ?tags=a&tags=b&tags=c
    IDs   []int    `query:"ids"`    // ?ids=1&ids=2&ids=3
}

Maps:

type Example struct {
    // Dot notation: ?metadata.key1=value1&metadata.key2=value2
    Metadata map[string]string `query:"metadata"`
    
    // Bracket notation: ?filters[status]=active&filters[type]=post
    Filters map[string]string `query:"filters"`
}

Struct Tags

enum - Enum Validation:

type Request struct {
    Status string `query:"status" enum:"active,inactive,pending"`
}

default - Default Values:

type Request struct {
    Limit int    `query:"limit" default:"10"`
    Sort  string `query:"sort" default:"desc"`
}

Combined:

type Request struct {
    Status string `query:"status" enum:"active,inactive" default:"active"`
    Limit  int    `query:"limit" default:"10"`
}

Complete Example

package main

import (
    "net/http"
    "rivaas.dev/router"
    "rivaas.dev/binding"
)

type CreateUserRequest struct {
    Name     string            `json:"name"`
    Email    string            `json:"email"`
    Age      int               `json:"age"`
    Tags     []string          `json:"tags"`
    Metadata map[string]string `json:"metadata"`
}

type SearchRequest struct {
    Query  string `query:"q"`
    Limit  int    `query:"limit" default:"10"`
    Status string `query:"status" enum:"active,inactive,all" default:"all"`
}

func main() {
    r := router.MustNew()
    
    // Strict JSON binding (built-in)
    r.POST("/users", func(c *router.Context) {
        var req CreateUserRequest
        if err := c.BindStrict(&req, router.BindOptions{MaxBytes: 1 << 20}); err != nil {
            return // Error already written
        }
        c.JSON(201, req)
    })
    
    // Query binding (using binding package)
    r.GET("/search", func(c *router.Context) {
        var req SearchRequest
        if err := binding.Query(c.Request, &req); err != nil {
            c.JSON(400, map[string]string{"error": err.Error()})
            return
        }
        c.JSON(200, req)
    })
    
    // Simple parameter access
    r.GET("/users/:id", func(c *router.Context) {
        userID := c.Param("id")
        includeDeleted := c.QueryDefault("include_deleted", "false")
        
        c.JSON(200, map[string]string{
            "user_id":         userID,
            "include_deleted": includeDeleted,
        })
    })
    
    http.ListenAndServe(":8080", r)
}

Next Steps

8 - Validation

Validate requests with multiple strategies: interface methods, struct tags, or JSON Schema.

Request validation ensures incoming data meets your requirements before processing.

Validation Strategies

Strategy Selection

Need complex business logic or request-scoped rules?
├─ Yes → Use Validate/ValidateContext interface methods
└─ No  → Continue ↓

Validating against external/shared schema?
├─ Yes → Use JSON Schema validation
└─ No  → Continue ↓

Simple field constraints (required, min, max, format)?
├─ Yes → Use struct tags (binding package + go-playground/validator)
└─ No  → Use manual validation

Interface Validation

Implement the Validate or ValidateContext interface on your request structs:

Basic Validation

type TransferRequest struct {
    FromAccount string  `json:"from_account"`
    ToAccount   string  `json:"to_account"`
    Amount      float64 `json:"amount"`
}

func (t *TransferRequest) Validate() error {
    if t.FromAccount == t.ToAccount {
        return errors.New("cannot transfer to same account")
    }
    if t.Amount > 10000 {
        return errors.New("amount exceeds daily limit")
    }
    return nil
}

Context-Aware Validation

type CreatePostRequest struct {
    Title string   `json:"title"`
    Tags  []string `json:"tags"`
}

func (p *CreatePostRequest) ValidateContext(ctx context.Context) error {
    // Get user tier from context
    tier := ctx.Value("user_tier")
    if tier == "free" && len(p.Tags) > 3 {
        return errors.New("free users can only use 3 tags")
    }
    return nil
}

Handler Integration

func createTransfer(c *router.Context) {
    var req TransferRequest
    if err := c.BindStrict(&req, router.BindOptions{MaxBytes: 1 << 20}); err != nil {
        return // Error response already written
    }
    
    // Call interface validation method
    if err := req.Validate(); err != nil {
        c.JSON(400, map[string]string{"error": err.Error()})
        return
    }
    
    // Process validated request
    c.JSON(200, map[string]string{"status": "success"})
}

Tag Validation with Binding Package

Use the binding package with struct tags for declarative validation:

import (
    "rivaas.dev/binding"
    "rivaas.dev/validation"
)

type CreateUserRequest struct {
    Email    string `json:"email" validate:"required,email"`
    Username string `json:"username" validate:"required,min=3,max=20"`
    Age      int    `json:"age" validate:"required,min=18,max=120"`
}

func createUser(c *router.Context) {
    var req CreateUserRequest
    
    // Bind JSON using binding package
    if err := binding.JSON(c.Request, &req); err != nil {
        c.JSON(400, map[string]string{"error": err.Error()})
        return
    }
    
    // Validate with struct tags
    if err := validation.Validate(&req); err != nil {
        c.JSON(400, map[string]string{"error": err.Error()})
        return
    }
    
    c.JSON(201, req)
}

Common Validation Tags

type Example struct {
    Required string  `validate:"required"`           // Must be present
    Email    string  `validate:"email"`              // Valid email format
    URL      string  `validate:"url"`                // Valid URL
    Min      int     `validate:"min=10"`             // Minimum value
    Max      int     `validate:"max=100"`            // Maximum value
    Range    int     `validate:"min=10,max=100"`     // Range
    Length   string  `validate:"min=3,max=50"`       // String length
    OneOf    string  `validate:"oneof=active pending"` // Enum
    Optional string  `validate:"omitempty,email"`    // Optional but validates if present
}

JSON Schema Validation

Implement the JSONSchemaProvider interface for contract-based validation:

type ProductRequest struct {
    Name  string  `json:"name"`
    Price float64 `json:"price"`
    SKU   string  `json:"sku"`
}

func (p *ProductRequest) JSONSchema() (id string, schema string) {
    return "product-v1", `{
        "type": "object",
        "properties": {
            "name": {"type": "string", "minLength": 3},
            "price": {"type": "number", "minimum": 0},
            "sku": {"type": "string", "pattern": "^[A-Z]{3}-[0-9]{6}$"}
        },
        "required": ["name", "price", "sku"]
    }`
}

Combining Binding and Validation

For a complete solution, combine strict binding with interface validation:

type CreateOrderRequest struct {
    CustomerID string       `json:"customer_id"`
    Items      []OrderItem  `json:"items"`
    Notes      string       `json:"notes"`
}

func (r *CreateOrderRequest) Validate() error {
    if len(r.Items) == 0 {
        return errors.New("order must have at least one item")
    }
    for i, item := range r.Items {
        if item.Quantity <= 0 {
            return fmt.Errorf("item %d: quantity must be positive", i)
        }
    }
    return nil
}

func createOrder(c *router.Context) {
    var req CreateOrderRequest
    
    // Strict JSON binding
    if err := c.BindStrict(&req, router.BindOptions{MaxBytes: 1 << 20}); err != nil {
        return // Error already written
    }
    
    // Business logic validation
    if err := req.Validate(); err != nil {
        c.JSON(400, map[string]string{"error": err.Error()})
        return
    }
    
    c.JSON(201, req)
}

Partial Validation (PATCH)

For PATCH requests, use pointer fields and check for presence:

type UpdateUserRequest struct {
    Email    *string `json:"email,omitempty"`
    Username *string `json:"username,omitempty"`
    Bio      *string `json:"bio,omitempty"`
}

func (r *UpdateUserRequest) Validate() error {
    if r.Email != nil && *r.Email == "" {
        return errors.New("email cannot be empty if provided")
    }
    if r.Username != nil && len(*r.Username) < 3 {
        return errors.New("username must be at least 3 characters")
    }
    if r.Bio != nil && len(*r.Bio) > 500 {
        return errors.New("bio cannot exceed 500 characters")
    }
    return nil
}

func updateUser(c *router.Context) {
    var req UpdateUserRequest
    
    if err := c.BindStrict(&req, router.BindOptions{}); err != nil {
        return
    }
    
    if err := req.Validate(); err != nil {
        c.JSON(400, map[string]string{"error": err.Error()})
        return
    }
    
    // Update only non-nil fields
    if req.Email != nil {
        // Update email
    }
    
    c.JSON(200, map[string]string{"status": "updated"})
}

Structured Validation Errors

Return detailed errors for better API usability:

type ValidationError struct {
    Field   string `json:"field"`
    Message string `json:"message"`
}

type ValidationErrors struct {
    Errors []ValidationError `json:"errors"`
}

func (r *CreateUserRequest) Validate() *ValidationErrors {
    var errs []ValidationError
    
    if r.Email == "" {
        errs = append(errs, ValidationError{
            Field:   "email",
            Message: "email is required",
        })
    }
    
    if len(r.Username) < 3 {
        errs = append(errs, ValidationError{
            Field:   "username",
            Message: "username must be at least 3 characters",
        })
    }
    
    if len(errs) > 0 {
        return &ValidationErrors{Errors: errs}
    }
    return nil
}

func createUser(c *router.Context) {
    var req CreateUserRequest
    
    if err := c.BindStrict(&req, router.BindOptions{}); err != nil {
        return
    }
    
    if verrs := req.Validate(); verrs != nil {
        c.JSON(400, verrs)
        return
    }
    
    c.JSON(201, req)
}

Best Practices

Do:

  • Use interface methods (Validate()) for business logic validation
  • Use pointer fields (*string) for optional PATCH fields
  • Return structured errors with field paths
  • Validate early, fail fast
  • Use BindStrict() for size limits and strict JSON parsing

Don’t:

  • Return sensitive data in validation error messages
  • Perform expensive validation (DB lookups) in Validate() - use ValidateContext() for those
  • Skip validation for internal endpoints

Complete Example

package main

import (
    "errors"
    "net/http"
    "rivaas.dev/router"
)

type CreateUserRequest struct {
    Email    string `json:"email"`
    Username string `json:"username"`
    Age      int    `json:"age"`
}

func (r *CreateUserRequest) Validate() error {
    if r.Email == "" {
        return errors.New("email is required")
    }
    if len(r.Username) < 3 {
        return errors.New("username must be at least 3 characters")
    }
    if r.Age < 18 || r.Age > 120 {
        return errors.New("age must be between 18 and 120")
    }
    return nil
}

func main() {
    r := router.MustNew()
    
    r.POST("/users", func(c *router.Context) {
        var req CreateUserRequest
        
        // Bind JSON with strict validation
        if err := c.BindStrict(&req, router.BindOptions{MaxBytes: 1 << 20}); err != nil {
            return // Error response already sent
        }
        
        // Run business validation
        if err := req.Validate(); err != nil {
            c.JSON(400, map[string]string{"error": err.Error()})
            return
        }
        
        c.JSON(201, req)
    })
    
    http.ListenAndServe(":8080", r)
}

Next Steps

9 - Response Rendering

Render JSON, YAML, HTML, binary responses with performance optimizations.

The router provides multiple response rendering methods optimized for different use cases.

JSON Variants

Standard JSON

HTML-escaped JSON (default):

c.JSON(200, map[string]string{
    "message": "Hello, <script>alert('xss')</script>",
})
// Output: {"message":"Hello, \u003cscript\u003ealert('xss')\u003c/script\u003e"}

Indented JSON

Pretty-printed for debugging:

c.IndentedJSON(200, data) // Pretty-printed with indentation

Pure JSON

No HTML escaping. 35% faster:

c.PureJSON(200, data) // Best for HTML/markdown content

Secure JSON

Anti-hijacking prefix for compliance:

c.SecureJSON(200, data) // Adds ")]}',\n" prefix

ASCII JSON

Pure ASCII with Unicode escaping:

c.AsciiJSON(200, data) // All Unicode as \uXXXX

JSONP

JSONP with callback:

c.JSONP(200, data, "callback") // callback({...})

Alternative Formats

YAML

c.YAML(200, config) // YAML rendering for config/DevOps APIs

Plain Text

c.String(200, "Hello, World!")
c.Stringf(200, "Hello, %s!", name)

HTML

c.HTML(200, "<h1>Welcome</h1>")

Binary & Streaming

Binary Data

c.Data(200, "image/png", imageBytes) // 98% faster than JSON!

Zero-Copy Streaming

file, _ := os.Open("video.mp4")
defer file.Close()
fileInfo, _ := file.Stat()

c.DataFromReader(200, fileInfo.Size(), "video/mp4", file, nil)

File Serving

c.ServeFile("/path/to/file.pdf")
c.Download("/path/to/file.pdf", "custom-name.pdf") // Force download

Performance Tips

Choose the Right Method

// Use PureJSON for HTML content (35% faster than JSON)
c.PureJSON(200, dataWithHTMLStrings)

// Use Data() for binary (98% faster than JSON)
c.Data(200, "image/png", imageBytes)

// Avoid YAML in hot paths (9x slower than JSON)
// c.YAML(200, data) // Only for config/admin endpoints

// Reserve IndentedJSON for debugging
// c.IndentedJSON(200, data) // Development only

Performance Benchmarks

Methodns/opOverhead vs JSONUse Case
JSON4,189-Production APIs
PureJSON2,725-35%HTML/markdown content
SecureJSON4,835+15%Compliance/old browsers
IndentedJSON8,111+94%Debug/development
AsciiJSON1,593-62%Legacy compatibility
YAML36,700+776%Config/admin APIs
Data90-98%Binary/custom formats

Complete Example

package main

import (
    "net/http"
    "rivaas.dev/router"
)

func main() {
    r := router.MustNew()
    
    // Standard JSON
    r.GET("/json", func(c *router.Context) {
        c.JSON(200, map[string]string{"message": "Hello"})
    })
    
    // Pure JSON (faster for HTML content)
    r.GET("/pure-json", func(c *router.Context) {
        c.PureJSON(200, map[string]string{
            "content": "<h1>Title</h1><p>Paragraph</p>",
        })
    })
    
    // YAML
    r.GET("/yaml", func(c *router.Context) {
        c.YAML(200, map[string]interface{}{
            "server": map[string]interface{}{
                "port": 8080,
                "host": "localhost",
            },
        })
    })
    
    // Binary data
    r.GET("/image", func(c *router.Context) {
        imageData := loadImage()
        c.Data(200, "image/png", imageData)
    })
    
    // File download
    r.GET("/download", func(c *router.Context) {
        c.Download("/path/to/report.pdf", "report-2024.pdf")
    })
    
    http.ListenAndServe(":8080", r)
}

Next Steps

10 - Content Negotiation

RFC 7231 compliant content negotiation for media types, charsets, encodings, and languages.

The router provides RFC 7231 compliant content negotiation through Accepts* methods.

Media Type Negotiation

r.GET("/data", func(c *router.Context) {
    if c.AcceptsJSON() {
        c.JSON(200, data)
    } else if c.Accepts("xml") != "" {
        c.XML(200, data)
    } else if c.AcceptsHTML() {
        c.HTML(200, htmlTemplate)
    } else {
        c.String(200, fmt.Sprintf("%v", data))
    }
})

Quality Values

// Request: Accept: application/json;q=0.8, text/html;q=1.0
r.GET("/content", func(c *router.Context) {
    accepted := c.Accepts("application/json", "text/html")
    // Returns "text/html" (higher quality value)
})

Wildcard Support

// Request: Accept: */*
c.Accepts("application/json") // true

// Request: Accept: text/*
c.Accepts("text/html", "text/plain") // Returns "text/html"

Character Set Negotiation

r.GET("/data", func(c *router.Context) {
    charset := c.AcceptsCharsets("utf-8", "iso-8859-1")
    // Set response charset based on preference
    c.Header("Content-Type", "text/html; charset="+charset)
})

Encoding Negotiation

r.GET("/data", func(c *router.Context) {
    encoding := c.AcceptsEncodings("gzip", "br", "deflate")
    if encoding == "gzip" {
        // Compress response with gzip
    } else if encoding == "br" {
        // Compress response with brotli
    }
})

Language Negotiation

r.GET("/content", func(c *router.Context) {
    lang := c.AcceptsLanguages("en-US", "en", "es", "fr")
    // Serve content in preferred language
    content := getContentInLanguage(lang)
    c.String(200, content)
})

Complete Example

package main

import (
    "net/http"
    "rivaas.dev/router"
)

type User struct {
    ID    string `json:"id" xml:"id"`
    Name  string `json:"name" xml:"name"`
    Email string `json:"email" xml:"email"`
}

func main() {
    r := router.MustNew()
    
    r.GET("/user/:id", func(c *router.Context) {
        user := User{
            ID:    c.Param("id"),
            Name:  "John Doe",
            Email: "john@example.com",
        }
        
        // Content negotiation
        if c.AcceptsJSON() {
            c.JSON(200, user)
        } else if c.Accepts("xml") != "" {
            c.XML(200, user)
        } else if c.AcceptsHTML() {
            html := fmt.Sprintf(`
                <div>
                    <h1>%s</h1>
                    <p>Email: %s</p>
                </div>
            `, user.Name, user.Email)
            c.HTML(200, html)
        } else {
            c.String(200, fmt.Sprintf("User: %s (%s)", user.Name, user.Email))
        }
    })
    
    http.ListenAndServe(":8080", r)
}

Next Steps

11 - API Versioning

How to version your API with Rivaas Router

This guide explains how to add versioning to your API. Versioning lets you change your API without breaking existing clients.

Why Version APIs?

You need API versioning when:

  • You remove or change fields — Breaks existing clients
  • You add required fields — Old clients don’t send them
  • You change behavior — Clients expect the old way
  • You want to test new features — Test with some users first
  • Clients upgrade slowly — Different clients use different versions

Versioning Methods

Rivaas Router supports four ways to detect versions:

The version goes in an HTTP header:

curl -H 'API-Version: v2' https://api.example.com/users

Good for:

  • Public APIs
  • RESTful services
  • Modern web applications

Why it’s good:

  • URLs stay clean
  • Works with CDN caching
  • Easy to route
  • Standard practice

2. Query Parameter Versioning

The version goes in the URL query:

curl 'https://api.example.com/users?version=v2'

Good for:

  • Developer testing
  • Internal APIs
  • Simple clients

Why it’s good:

  • Easy to test in browsers
  • Simple to document
  • No header handling needed

3. Path-Based Versioning

The version goes in the URL path:

curl https://api.example.com/v2/users

Good for:

  • Very different API versions
  • Simple routing
  • When you want version visible

Why it’s good:

  • Most visible
  • Works with all HTTP clients
  • Easy infrastructure routing

4. Accept Header Versioning

The version goes in the Accept header (content negotiation):

curl -H 'Accept: application/vnd.myapi.v2+json' https://api.example.com/users

Good for:

  • Hypermedia APIs
  • Multiple content types
  • Strict REST compliance

Why it’s good:

  • Follows HTTP standards
  • Supports content negotiation
  • Used by major APIs

Getting Started

Basic Setup

Here’s how to set up versioning:

package main

import (
    "net/http"
    "rivaas.dev/router"
)

func main() {
    r := router.New(
        router.WithVersioning(
            // Choose your version detection method
            router.WithHeaderVersioning("API-Version"),
            
            // Set default version (when client doesn't specify)
            router.WithDefaultVersion("v2"),
            
            // Optional: Only allow these versions
            router.WithValidVersions("v1", "v2", "v3"),
        ),
    )
    
    // Create version 1 routes
    v1 := r.Version("v1")
    v1.GET("/users", listUsersV1)
    
    // Create version 2 routes
    v2 := r.Version("v2")
    v2.GET("/users", listUsersV2)
    
    http.ListenAndServe(":8080", r)
}

Using Multiple Methods

You can enable multiple detection methods. The router checks them in order:

r := router.New(
    router.WithVersioning(
        router.WithHeaderVersioning("API-Version"),       // Primary
        router.WithQueryVersioning("version"),           // For testing
        router.WithPathVersioning("/v{version}/"),       // Legacy support
        router.WithAcceptVersioning("application/vnd.myapi.v{version}+json"),
        router.WithDefaultVersion("v2"),
    ),
)

Check order (first match wins):

  1. Custom detector (if you made one)
  2. Accept header
  3. Path parameter
  4. HTTP header
  5. Query parameter
  6. Default version

Version Detection Methods

Header-Based

Configure:

router.WithHeaderVersioning("API-Version")

Clients use:

curl -H 'API-Version: v2' https://api.example.com/users

Query Parameter

Configure:

router.WithQueryVersioning("version")

Clients use:

curl 'https://api.example.com/users?version=v2'

Path-Based

Configure:

router.WithPathVersioning("/v{version}/")

Routes work with or without path version:

// Accessed as /v2/users or /users (with header/query)
r.Version("v2").GET("/users", handler)

Clients use:

curl https://api.example.com/v2/users

Accept Header

Configure:

router.WithAcceptVersioning("application/vnd.myapi.v{version}+json")

Clients use:

curl -H 'Accept: application/vnd.myapi.v2+json' https://api.example.com/users

Custom Detector

For complex logic, make your own detector:

router.WithCustomVersionDetector(func(req *http.Request) string {
    // Your custom logic
    if isLegacyClient(req) {
        return "v1"
    }
    return extractVersionSomehow(req)
})

Migration Patterns

Share Business Logic

Keep business logic the same, change only the response format:

// Business logic (shared between versions)
func getUserByID(id string) (*User, error) {
    // Database query, business rules, etc.
    return &User{ID: id, Name: "Alice"}, nil
}

// Version 1 handler
func listUsersV1(c *router.Context) {
    users, _ := getUsersFromDB()
    
    // V1 format: flat structure
    c.JSON(200, map[string]any{
        "users": users,
    })
}

// Version 2 handler
func listUsersV2(c *router.Context) {
    users, _ := getUsersFromDB()
    
    // V2 format: with metadata
    c.JSON(200, map[string]any{
        "data": users,
        "meta": map[string]any{
            "total": len(users),
            "version": "v2",
        },
    })
}

Handle Breaking Changes

Example: Making email field required

Version 1 (original):

type UserV1 struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email,omitempty"` // Optional
}

Version 2 (breaking change):

type UserV2 struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"` // Required now
}

func createUserV2(c *router.Context) {
    var user UserV2
    if err := c.Bind(&user); err != nil {
        c.JSON(400, map[string]string{
            "error": "validation failed",
            "detail": "email is required in API v2",
        })
        return
    }
    
    // Create user...
}

Version-Specific Middleware

Apply different middleware to different versions:

v1 := r.Version("v1")
v1.Use(legacyAuthMiddleware)
v1.GET("/users", listUsersV1)

v2 := r.Version("v2")
v2.Use(jwtAuthMiddleware)  // Different auth method
v2.GET("/users", listUsersV2)

Change Data Structure

Example: Flat to nested structure

// V1: Flat structure
type UserV1 struct {
    ID      int    `json:"id"`
    Name    string `json:"name"`
    City    string `json:"city"`
    Country string `json:"country"`
}

// V2: Nested structure
type UserV2 struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
    Address struct {
        City    string `json:"city"`
        Country string `json:"country"`
    } `json:"address"`
}

// Helper to convert
func convertV1ToV2(v1 UserV1) UserV2 {
    v2 := UserV2{
        ID:   v1.ID,
        Name: v1.Name,
    }
    v2.Address.City = v1.City
    v2.Address.Country = v1.Country
    return v2
}

Deprecation Strategy

Mark Versions as Deprecated

Tell the router when a version should stop working:

r := router.New(
    router.WithVersioning(
        // Mark v1 as deprecated with end date
        router.WithDeprecatedVersion(
            "v1",
            time.Date(2025, 12, 31, 23, 59, 59, 0, time.UTC),
        ),
        
        // Track version usage
        router.WithVersionObserver(
            router.WithOnDetected(func(version, method string) {
                // Record metrics
                metrics.RecordVersionUsage(version, method)
            }),
            router.WithOnMissing(func() {
                // Client didn't specify version
                log.Warn("client using default version")
            }),
            router.WithOnInvalid(func(attempted string) {
                // Client used invalid version
                metrics.RecordInvalidVersion(attempted)
            }),
        ),
    ),
)

Deprecation Headers

The router automatically adds headers for deprecated versions:

Sunset: Wed, 31 Dec 2025 23:59:59 GMT
Deprecation: true
Link: <https://api.example.com/docs/migration>; rel="deprecation"

These tell clients when the version will stop working.

Deprecation Timeline

6 months before end:

  1. Announce in release notes
  2. Add deprecation header
  3. Write migration guide
  4. Contact major users

3 months before end:

  1. Add sunset header with date
  2. Email active users
  3. Monitor usage (should go down)
  4. Offer help with migration

1 month before end:

  1. Send final warnings
  2. Return 410 Gone for deprecated endpoints
  3. Link to migration guide

After end date:

  1. Remove old version code
  2. Always return 410 Gone
  3. Keep migration documentation

Best Practices

1. Use Semantic Versioning

  • Major (v1, v2, v3): Breaking changes
  • Minor (v2.1, v2.2): New features, backward compatible
  • Patch (v2.1.1): Bug fixes only

2. Know When to Version

Don’t version for:

  • Bug fixes
  • Performance improvements
  • Internal refactoring
  • Adding optional fields
  • Making validation less strict

Do version for:

  • Removing fields
  • Changing field types
  • Making optional field required
  • Major behavior changes
  • Changing error codes

3. Keep Backward Compatibility

// Good: Add optional field
type UserV2 struct {
    ID    int     `json:"id"`
    Name  string  `json:"name"`
    Email string  `json:"email,omitempty"` // New, optional
}

// Bad: Remove field (breaks clients)
type UserV2 struct {
    ID   int    `json:"id"`
    // Name removed - BREAKING CHANGE!
}

4. Document Version Differences

Keep clear documentation for each version:

## API Versions

### v2 (Current)
- Added email field (optional)
- Added address nested object
- Added PATCH support for partial updates

### v1 (Deprecated - Ends 2025-12-31)
- Original API
- Only GET/POST/PUT/DELETE
- Flat structure only

5. Organize Routes by Version

Group version routes together:

v1 := r.Version("v1")
{
    v1.GET("/users", listUsersV1)
    v1.GET("/users/:id", getUserV1)
    v1.POST("/users", createUserV1)
}

v2 := r.Version("v2")
{
    v2.GET("/users", listUsersV2)
    v2.GET("/users/:id", getUserV2)
    v2.POST("/users", createUserV2)
    v2.PATCH("/users/:id", updateUserV2) // New in v2
}

6. Validate Versions

Reject invalid versions early:

router.WithVersioning(
    router.WithValidVersions("v1", "v2", "v3", "beta"),
    router.WithVersionObserver(
        router.WithOnInvalid(func(attempted string) {
            log.Warn("invalid API version", "version", attempted)
        }),
    ),
)

7. Test All Versions

func TestAPIVersions(t *testing.T) {
    r := setupRouter()
    
    tests := []struct{
        version string
        path    string
        want    int
    }{
        {"v1", "/users", 200},
        {"v2", "/users", 200},
        {"v3", "/users", 200},
        {"v99", "/users", 404}, // Invalid
    }
    
    for _, tt := range tests {
        req := httptest.NewRequest("GET", tt.path, nil)
        req.Header.Set("API-Version", tt.version)
        
        w := httptest.NewRecorder()
        r.ServeHTTP(w, req)
        
        assert.Equal(t, tt.want, w.Code)
    }
}

Real-World Examples

Stripe-Style (Date-Based Versions)

r := router.New(
    router.WithVersioning(
        router.WithHeaderVersioning("Stripe-Version"),
        router.WithDefaultVersion("2024-11-20"),
        router.WithValidVersions(
            "2024-11-20",
            "2024-10-28",
            "2024-09-30",
        ),
    ),
)

// Version by date
v20241120 := r.Version("2024-11-20")
v20241120.GET("/charges", listCharges)

GitHub-Style (Accept Header)

r := router.New(
    router.WithVersioning(
        router.WithAcceptVersioning("application/vnd.github.v{version}+json"),
        router.WithDefaultVersion("v3"),
    ),
)

// Usage: Accept: application/vnd.github.v3+json

Further Reading

Summary

API versioning helps you:

  • Make changes without breaking clients
  • Support old and new clients at the same time
  • Control when to remove old versions
  • Track which versions clients use

Choose header-based versioning for most cases. Use query parameters for testing. Document your changes clearly. Give clients time to migrate before removing old versions.

12 - Observability

Native OpenTelemetry tracing support with zero overhead when disabled, plus diagnostic events.

The router includes native OpenTelemetry tracing support and optional diagnostic events.

OpenTelemetry Tracing

Enable Tracing

r := router.New(router.WithTracing())

Configuration Options

r := router.New(
    router.WithTracing(),
    router.WithTracingServiceName("my-api"),
    router.WithTracingServiceVersion("v1.2.3"),
    router.WithTracingSampleRate(0.1), // 10% sampling
    router.WithTracingExcludePaths("/health", "/metrics"),
)

Context Tracing Methods

func handler(c *router.Context) {
    // Get trace/span IDs
    traceID := c.TraceID()
    spanID := c.SpanID()
    
    // Add custom attributes
    c.SetSpanAttribute("user.id", "123")
    c.SetSpanAttribute("operation.type", "database_query")
    
    // Add events
    c.AddSpanEvent("processing_started")
    c.AddSpanEvent("cache_miss", 
        attribute.String("cache.key", "user:123"),
    )
}

Complete Tracing Example

package main

import (
    "context"
    "log"
    "net/http"
    
    "rivaas.dev/router"
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/exporters/jaeger"
    "go.opentelemetry.io/otel/sdk/trace"
)

func main() {
    // Initialize Jaeger exporter
    exp, err := jaeger.New(jaeger.WithCollectorEndpoint(
        jaeger.WithEndpoint("http://localhost:14268/api/traces"),
    ))
    if err != nil {
        log.Fatal(err)
    }

    tp := trace.NewTracerProvider(
        trace.WithBatcher(exp),
        trace.WithSampler(trace.TraceIDRatioBased(0.1)),
    )
    otel.SetTracerProvider(tp)

    // Create router with tracing
    r := router.New(
        router.WithTracing(),
        router.WithTracingServiceName("my-service"),
    )
    
    r.GET("/", func(c *router.Context) {
        c.SetSpanAttribute("handler", "home")
        c.JSON(200, map[string]string{"message": "Hello"})
    })
    
    defer tp.Shutdown(context.Background())
    log.Fatal(http.ListenAndServe(":8080", r))
}

Diagnostics

Enable diagnostic events for security concerns and configuration issues:

import "log/slog"

handler := router.DiagnosticHandlerFunc(func(e router.DiagnosticEvent) {
    slog.Warn(e.Message, "kind", e.Kind, "fields", e.Fields)
})

r := router.New(router.WithDiagnostics(handler))

Diagnostic Event Types

  • DiagXFFSuspicious - Suspicious X-Forwarded-For chain detected
  • DiagHeaderInjection - Header injection attempt blocked
  • DiagInvalidProto - Invalid X-Forwarded-Proto value
  • DiagHighParamCount - Route has >8 parameters (uses map storage)
  • DiagH2CEnabled - H2C enabled (development warning)

Example with Metrics

handler := router.DiagnosticHandlerFunc(func(e router.DiagnosticEvent) {
    metrics.Increment("router.diagnostics", "kind", string(e.Kind))
})

r := router.New(router.WithDiagnostics(handler))

Best Practices

  1. Use path exclusion for high-frequency endpoints:

    router.WithTracingExcludePaths("/health", "/metrics", "/ping")
    
  2. Set appropriate sampling rates in production:

    router.WithTracingSampleRate(0.01) // 1% sampling
    
  3. Add meaningful attributes in handlers:

    c.SetSpanAttribute("user.id", userID)
    c.SetSpanAttribute("operation.type", "database_query")
    
  4. Disable parameter recording for sensitive data:

    router.WithTracingDisableParams()
    

Next Steps

13 - Static Files

Serve static files and directories efficiently.

The router provides methods for serving static files and directories.

Directory Serving

Serve an entire directory.

r := router.MustNew()

// Serve ./public/* at /assets/*
r.Static("/assets", "./public")

// Serve /var/uploads/* at /uploads/*
r.Static("/uploads", "/var/uploads")

Example:

./public/
├── css/
│   └── style.css
├── js/
│   └── app.js
└── images/
    └── logo.png

Access:

  • http://localhost:8080/assets/css/style.css
  • http://localhost:8080/assets/js/app.js
  • http://localhost:8080/assets/images/logo.png

Single File Serving

Serve specific files:

r.StaticFile("/favicon.ico", "./static/favicon.ico")
r.StaticFile("/robots.txt", "./static/robots.txt")

Custom File System

Use a custom filesystem.

import "net/http"

r.StaticFS("/files", http.Dir("./files"))

Embedded Files

Go 1.16 added embed.FS which lets you put files inside your binary. This is great for single-file deployments — no need to copy static files around.

The router has a helper method that makes this easy:

import "embed"

//go:embed web/dist/*
var webAssets embed.FS

r := router.MustNew()

// Serve web/dist/* at /assets/*
r.StaticEmbed("/assets", webAssets, "web/dist")

The third parameter ("web/dist") tells the router which folder inside the embed to use. This strips that prefix from the URLs.

Why use embedded files?

  • One binary — Deploy a single file, no folders to manage
  • Fast startup — Files are already in memory
  • Safe — Nobody can change your static files at runtime

Example project layout:

myapp/
├── main.go
└── web/
    └── dist/
        ├── index.html
        ├── css/
        │   └── style.css
        └── js/
            └── app.js

Serving a frontend app:

package main

import (
    "embed"
    "net/http"
    "rivaas.dev/router"
)

//go:embed web/dist/*
var webAssets embed.FS

func main() {
    r := router.MustNew()
    
    // Serve your frontend at the root
    r.StaticEmbed("/", webAssets, "web/dist")
    
    // API routes
    r.GET("/api/status", func(c *router.Context) {
        c.JSON(200, map[string]string{"status": "OK"})
    })
    
    http.ListenAndServe(":8080", r)
}

Now http://localhost:8080/ serves index.html, and http://localhost:8080/css/style.css serves your CSS.

File Serving in Handlers

Serve File

r.GET("/download/:filename", func(c *router.Context) {
    filename := c.Param("filename")
    filepath := "./uploads/" + filename
    c.ServeFile(filepath)
})

Force Download

r.GET("/download/:filename", func(c *router.Context) {
    filename := c.Param("filename")
    filepath := "./reports/" + filename
    c.Download(filepath, "report-2024.pdf")
})

Wildcard Routes for File Serving

r.GET("/files/*filepath", func(c *router.Context) {
    filepath := c.Param("filepath")
    fullPath := "./public/" + filepath
    c.ServeFile(fullPath)
})

Complete Example

Here’s a full example with all the ways to serve static files:

package main

import (
    "embed"
    "net/http"
    "rivaas.dev/router"
)

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

func main() {
    r := router.MustNew()
    
    // Option 1: Serve from filesystem
    r.Static("/assets", "./public")
    
    // Option 2: Serve embedded files
    r.StaticEmbed("/static", staticAssets, "static")
    
    // Serve specific files
    r.StaticFile("/favicon.ico", "./static/favicon.ico")
    r.StaticFile("/robots.txt", "./static/robots.txt")
    
    // Custom file serving with download
    r.GET("/downloads/:filename", func(c *router.Context) {
        filename := c.Param("filename")
        c.Download("./files/"+filename, filename)
    })
    
    // API routes
    r.GET("/api/status", func(c *router.Context) {
        c.JSON(200, map[string]string{"status": "OK"})
    })
    
    http.ListenAndServe(":8080", r)
}

Security Considerations

Path Traversal Prevention

// ❌ BAD: Vulnerable to path traversal
r.GET("/files/*filepath", func(c *router.Context) {
    filepath := c.Param("filepath")
    c.ServeFile(filepath) // Can access ../../../etc/passwd
})

// ✅ GOOD: Validate and sanitize paths
r.GET("/files/*filepath", func(c *router.Context) {
    filepath := c.Param("filepath")
    
    // Validate path
    if strings.Contains(filepath, "..") {
        c.Status(400)
        return
    }
    
    // Serve from safe directory
    c.ServeFile("./public/" + filepath)
})

Best Practices

  1. Use absolute paths for static directories
  2. Validate file paths to prevent traversal attacks
  3. Set appropriate cache headers for static assets
  4. Use CDN for production static assets
  5. Serve from dedicated file server for large files
  6. Use embed.FS for single-binary deployments (great for containers and CLI tools)

Next Steps

14 - Testing

Test your routes and middleware with httptest.

Testing router-based applications is straightforward using Go’s httptest package.

Testing Routes

Basic Route Test

package main

import (
    "net/http"
    "net/http/httptest"
    "testing"
    
    "rivaas.dev/router"
)

func TestGetUser(t *testing.T) {
    r := router.MustNew()
    r.GET("/users/:id", func(c *router.Context) {
        c.JSON(200, map[string]string{
            "user_id": c.Param("id"),
        })
    })
    
    req := httptest.NewRequest("GET", "/users/123", nil)
    w := httptest.NewRecorder()
    
    r.ServeHTTP(w, req)
    
    if w.Code != http.StatusOK {
        t.Errorf("Expected status 200, got %d", w.Code)
    }
}

Testing JSON Responses

func TestCreateUser(t *testing.T) {
    r := router.MustNew()
    r.POST("/users", func(c *router.Context) {
        c.JSON(201, map[string]string{"id": "123"})
    })
    
    body := strings.NewReader(`{"name":"John"}`)
    req := httptest.NewRequest("POST", "/users", body)
    req.Header.Set("Content-Type", "application/json")
    
    w := httptest.NewRecorder()
    r.ServeHTTP(w, req)
    
    if w.Code != 201 {
        t.Errorf("Expected status 201, got %d", w.Code)
    }
    
    var response map[string]string
    if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
        t.Fatal(err)
    }
    
    if response["id"] != "123" {
        t.Errorf("Expected id '123', got %v", response["id"])
    }
}

Testing Middleware

func TestAuthMiddleware(t *testing.T) {
    r := router.MustNew()
    r.Use(AuthRequired())
    r.GET("/protected", func(c *router.Context) {
        c.JSON(200, map[string]string{"message": "success"})
    })
    
    // Test without auth header
    req := httptest.NewRequest("GET", "/protected", nil)
    w := httptest.NewRecorder()
    r.ServeHTTP(w, req)
    
    if w.Code != 401 {
        t.Errorf("Expected status 401, got %d", w.Code)
    }
    
    // Test with auth header
    req = httptest.NewRequest("GET", "/protected", nil)
    req.Header.Set("Authorization", "Bearer valid-token")
    w = httptest.NewRecorder()
    r.ServeHTTP(w, req)
    
    if w.Code != 200 {
        t.Errorf("Expected status 200, got %d", w.Code)
    }
}

Table-Driven Tests

func TestRoutes(t *testing.T) {
    r := setupRouter()
    
    tests := []struct {
        name           string
        method         string
        path           string
        expectedStatus int
    }{
        {"Home", "GET", "/", 200},
        {"Users", "GET", "/users", 200},
        {"Not Found", "GET", "/invalid", 404},
        {"Method Not Allowed", "POST", "/", 405},
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            req := httptest.NewRequest(tt.method, tt.path, nil)
            w := httptest.NewRecorder()
            
            r.ServeHTTP(w, req)
            
            if w.Code != tt.expectedStatus {
                t.Errorf("Expected status %d, got %d", tt.expectedStatus, w.Code)
            }
        })
    }
}

Helper Functions

func setupRouter() *router.Router {
    r := router.MustNew()
    r.GET("/users", listUsers)
    r.POST("/users", createUser)
    r.GET("/users/:id", getUser)
    return r
}

func makeRequest(r *router.Router, method, path string, body io.Reader) *httptest.ResponseRecorder {
    req := httptest.NewRequest(method, path, body)
    w := httptest.NewRecorder()
    r.ServeHTTP(w, req)
    return w
}

Complete Test Example

package main

import (
    "bytes"
    "encoding/json"
    "net/http"
    "net/http/httptest"
    "testing"
    
    "rivaas.dev/router"
)

func setupRouter() *router.Router {
    r := router.MustNew()
    r.GET("/users", listUsers)
    r.POST("/users", createUser)
    r.GET("/users/:id", getUser)
    return r
}

func TestListUsers(t *testing.T) {
    r := setupRouter()
    
    req := httptest.NewRequest("GET", "/users", nil)
    w := httptest.NewRecorder()
    
    r.ServeHTTP(w, req)
    
    if w.Code != 200 {
        t.Errorf("Expected 200, got %d", w.Code)
    }
}

func TestCreateUser(t *testing.T) {
    r := setupRouter()
    
    userData := map[string]string{
        "name":  "John Doe",
        "email": "john@example.com",
    }
    
    body, _ := json.Marshal(userData)
    req := httptest.NewRequest("POST", "/users", bytes.NewBuffer(body))
    req.Header.Set("Content-Type", "application/json")
    
    w := httptest.NewRecorder()
    r.ServeHTTP(w, req)
    
    if w.Code != 201 {
        t.Errorf("Expected 201, got %d", w.Code)
    }
}

func TestGetUser(t *testing.T) {
    r := setupRouter()
    
    req := httptest.NewRequest("GET", "/users/123", nil)
    w := httptest.NewRecorder()
    
    r.ServeHTTP(w, req)
    
    if w.Code != 200 {
        t.Errorf("Expected 200, got %d", w.Code)
    }
}

// Handlers (simplified for testing)
func listUsers(c *router.Context) {
    c.JSON(200, []string{"user1", "user2"})
}

func createUser(c *router.Context) {
    c.JSON(201, map[string]string{"id": "123"})
}

func getUser(c *router.Context) {
    c.JSON(200, map[string]string{"id": c.Param("id")})
}

Best Practices

  1. Use table-driven tests for multiple scenarios
  2. Test error cases not just success paths
  3. Mock dependencies for unit tests
  4. Use test helpers to reduce boilerplate
  5. Test middleware independently from routes

Next Steps

15 - Migration

Migrate from Gin, Echo, or http.ServeMux to Rivaas Router.

This guide helps you migrate from other popular Go routers.

Route Registration

gin := gin.Default()
gin.GET("/users/:id", getUserHandler)
gin.POST("/users", createUserHandler)
e := echo.New()
e.GET("/users/:id", getUserHandler)
e.POST("/users", createUserHandler)
mux := http.NewServeMux()
mux.HandleFunc("/users/", usersHandler)
mux.HandleFunc("/posts/", postsHandler)
r := router.MustNew()
r.GET("/users/:id", getUserHandler)
r.POST("/users", createUserHandler)

Context Usage

func ginHandler(c *gin.Context) {
    id := c.Param("id")
    c.JSON(200, gin.H{"user_id": id})
}
func echoHandler(c echo.Context) error {
    id := c.Param("id")
    return c.JSON(200, map[string]string{"user_id": id})
}
func usersHandler(w http.ResponseWriter, r *http.Request) {
    path := strings.TrimPrefix(r.URL.Path, "/users/")
    userID := strings.Split(path, "/")[0]
    // Manual parameter extraction
}
func rivaasHandler(c *router.Context) {
    id := c.Param("id")
    c.JSON(200, map[string]string{"user_id": id})
}

Middleware

gin.Use(gin.Logger(), gin.Recovery())
e.Use(middleware.Logger())
e.Use(middleware.Recover())
// Manual middleware chaining
handler := Logger(Recovery(mux))
http.ListenAndServe(":8080", handler)
r.Use(Logger(), Recovery())

Key Differences

Response Methods

FeatureGinEchoRivaas
JSONc.JSON()c.JSON()c.JSON()
Stringc.String()c.String()c.String()
HTMLc.HTML()c.HTML()c.HTML()
Pure JSON
Secure JSON
YAML

Request Binding

FeatureGinEchoRivaas
Query binding
JSON binding
Form binding
Maps (dot notation)
Maps (bracket notation)
Nested structs in query
Enum validation
Default values

Next Steps

16 - Examples

Complete working examples and common use cases.

This guide provides complete, working examples for common use cases.

REST API Server

Complete REST API with CRUD operations:

package main

import (
    "encoding/json"
    "net/http"
    "rivaas.dev/router"
)

type User struct {
    ID    string `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

var users = map[string]User{
    "1": {ID: "1", Name: "Alice", Email: "alice@example.com"},
    "2": {ID: "2", Name: "Bob", Email: "bob@example.com"},
}

func main() {
    r := router.MustNew()
    r.Use(Logger(), Recovery(), CORS())
    
    api := r.Group("/api/v1")
    api.Use(JSONContentType())
    {
        api.GET("/users", listUsers)
        api.POST("/users", createUser)
        api.GET("/users/:id", getUser)
        api.PUT("/users/:id", updateUser)
        api.DELETE("/users/:id", deleteUser)
    }
    
    http.ListenAndServe(":8080", r)
}

func listUsers(c *router.Context) {
    userList := make([]User, 0, len(users))
    for _, user := range users {
        userList = append(userList, user)
    }
    c.JSON(200, userList)
}

func getUser(c *router.Context) {
    id := c.Param("id")
    user, exists := users[id]
    if !exists {
        c.JSON(404, map[string]string{"error": "User not found"})
        return
    }
    c.JSON(200, user)
}

func createUser(c *router.Context) {
    var user User
    if err := json.NewDecoder(c.Request.Body).Decode(&user); err != nil {
        c.JSON(400, map[string]string{"error": "Invalid JSON"})
        return
    }
    users[user.ID] = user
    c.JSON(201, user)
}

func updateUser(c *router.Context) {
    id := c.Param("id")
    if _, exists := users[id]; !exists {
        c.JSON(404, map[string]string{"error": "User not found"})
        return
    }
    
    var user User
    if err := json.NewDecoder(c.Request.Body).Decode(&user); err != nil {
        c.JSON(400, map[string]string{"error": "Invalid JSON"})
        return
    }
    
    user.ID = id
    users[id] = user
    c.JSON(200, user)
}

func deleteUser(c *router.Context) {
    id := c.Param("id")
    if _, exists := users[id]; !exists {
        c.JSON(404, map[string]string{"error": "User not found"})
        return
    }
    delete(users, id)
    c.Status(204)
}

Microservice Gateway

API gateway with service routing:

package main

import (
    "net/http"
    "rivaas.dev/router"
)

func main() {
    r := router.MustNew()
    r.Use(Logger(), RateLimit(), Tracing())
    
    // Service discovery and routing
    r.GET("/users/*path", proxyToUserService)
    r.GET("/orders/*path", proxyToOrderService)
    r.GET("/payments/*path", proxyToPaymentService)
    
    // Health checks
    r.GET("/health", healthCheck)
    r.GET("/metrics", metricsHandler)
    
    http.ListenAndServe(":8080", r)
}

func proxyToUserService(c *router.Context) {
    path := c.Param("path")
    // Proxy to user service...
    c.JSON(200, map[string]string{"service": "users", "path": path})
}

func proxyToOrderService(c *router.Context) {
    path := c.Param("path")
    // Proxy to order service...
    c.JSON(200, map[string]string{"service": "orders", "path": path})
}

func proxyToPaymentService(c *router.Context) {
    path := c.Param("path")
    // Proxy to payment service...
    c.JSON(200, map[string]string{"service": "payments", "path": path})
}

func healthCheck(c *router.Context) {
    c.JSON(200, map[string]string{"status": "OK"})
}

func metricsHandler(c *router.Context) {
    c.String(200, "# HELP requests_total Total requests\n# TYPE requests_total counter\n")
}

Static File Server with API

Serve static files alongside API routes:

package main

import (
    "net/http"
    "rivaas.dev/router"
)

func main() {
    r := router.MustNew()
    
    // Serve static files
    r.Static("/assets", "./public")
    r.StaticFile("/favicon.ico", "./static/favicon.ico")
    
    // API routes
    api := r.Group("/api")
    {
        api.GET("/status", statusHandler)
        api.GET("/users", listUsersHandler)
    }
    
    http.ListenAndServe(":8080", r)
}

func statusHandler(c *router.Context) {
    c.JSON(200, map[string]string{"status": "OK"})
}

func listUsersHandler(c *router.Context) {
    c.JSON(200, []string{"user1", "user2"})
}

Authentication & Authorization

Complete auth example with JWT:

package main

import (
    "net/http"
    "rivaas.dev/router"
)

func main() {
    r := router.MustNew()
    r.Use(Logger(), Recovery())
    
    // Public routes
    r.POST("/login", loginHandler)
    r.POST("/register", registerHandler)
    
    // Protected routes
    api := r.Group("/api")
    api.Use(JWTAuth())
    {
        api.GET("/profile", profileHandler)
        api.PUT("/profile", updateProfileHandler)
        
        // Admin routes
        admin := api.Group("/admin")
        admin.Use(RequireAdmin())
        {
            admin.GET("/users", listUsersHandler)
            admin.DELETE("/users/:id", deleteUserHandler)
        }
    }
    
    http.ListenAndServe(":8080", r)
}

func loginHandler(c *router.Context) {
    // Authenticate user and generate JWT...
    c.JSON(200, map[string]string{"token": "jwt-token-here"})
}

func registerHandler(c *router.Context) {
    // Create new user...
    c.JSON(201, map[string]string{"message": "User created"})
}

func profileHandler(c *router.Context) {
    // Get user from context (set by JWT middleware)
    c.JSON(200, map[string]string{"user": "john@example.com"})
}

func updateProfileHandler(c *router.Context) {
    c.JSON(200, map[string]string{"message": "Profile updated"})
}

func listUsersHandler(c *router.Context) {
    c.JSON(200, []string{"user1", "user2"})
}

func deleteUserHandler(c *router.Context) {
    c.Status(204)
}

func JWTAuth() router.HandlerFunc {
    return func(c *router.Context) {
        token := c.Request.Header.Get("Authorization")
        if token == "" {
            c.JSON(401, map[string]string{"error": "Unauthorized"})
            return
        }
        // Validate JWT...
        c.Next()
    }
}

func RequireAdmin() router.HandlerFunc {
    return func(c *router.Context) {
        // Check if user is admin...
        c.Next()
    }
}

Next Steps