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

Return to the regular view of this page.

Router Package

Complete API reference for the rivaas.dev/router package.

This is the API reference for the rivaas.dev/router package. For learning-focused documentation, see the Router Guide.

Overview

The rivaas.dev/router package provides a high-performance HTTP router with comprehensive features:

  • Radix tree routing with bloom filters
  • Optional compiled route tables for large route sets
  • Built-in middleware support
  • OpenTelemetry support
  • API versioning
  • Content negotiation

Package Structure

rivaas.dev/router/
├── router.go          # Core router and route registration
├── context.go         # Request context with pooling
├── serve.go           # Request serving and dispatch
├── routes.go          # Route tree and method dispatch
├── radix.go           # Radix tree and route matching
├── route_bridge.go    # Route groups and mounting
├── options.go         # Router options
├── route/             # Route definitions and constraints
│   ├── route.go
│   ├── constraint.go
│   ├── group.go
│   └── ...
├── compiler/          # Optional compiled route lookup
├── version/           # API versioning
└── ...

Middleware (accesslog, cors, recovery, etc.) lives in separate packages under rivaas.dev/middleware/, not inside the router package.

Quick API Index

Core Types

Route Registration

  • HTTP Methods: GET(), POST(), PUT(), DELETE(), PATCH(), OPTIONS(), HEAD()
  • Route Groups: Group(prefix), Version(version)
  • Middleware: Use(middleware...)
  • Static Files: Static(), StaticFile(), StaticFS()

Request Handling

  • Parameters: Param(), Query(), PostForm()
  • Headers: Header(), GetHeader()
  • Cookies: Cookie(), SetCookie()

Response Rendering

  • JSON: JSON(), PureJSON(), IndentedJSON(), SecureJSON()
  • Other: YAML(), String(), HTML(), Data()
  • Files: ServeFile(), Download(), DataFromReader()

Configuration

Performance

Routing Performance

  • Sub-microsecond routing — See Performance for current latency and throughput numbers.
  • Zero allocation — No allocations for routing and param extraction in typical cases (≤8 path params). See Performance for benchmark details.
  • Memory efficient — Context pooling and minimal allocations per request.
  • Context pooling: Automatic context reuse
  • 404 handling: A single pooled context and conditional dispatch for custom NoRoute handler vs default RFC 9457 response
  • Lock-free operations: Atomic operations for concurrent access

Optimization Features

  • Optional compiled routes: Pre-compiled lookups for large APIs (opt-in via WithRouteCompilation(true))
  • Bloom filters: Fast negative lookups when compiled routes are enabled
  • First-segment index: ASCII-only route narrowing (O(1) lookup)
  • Parameter storage: Array-based for ≤8 params, map for >8
  • Type caching: Reflection information cached per struct type

Thread Safety

All router operations are concurrent-safe:

  • Route registration can occur from multiple goroutines
  • Route trees use atomic operations for concurrent access
  • Context pooling is thread-safe
  • Middleware execution is goroutine-safe

Next Steps

1 - API Reference

Core types and methods for the router package.

Router

router.New(opts ...Option) *Router

Creates a new router instance.

r := router.New()

// With options
r := router.New(
    router.WithTracing(),
    router.WithTracingServiceName("my-api"),
)

HTTP Method Handlers

Register routes for HTTP methods:

r.GET(path string, handlers ...HandlerFunc) *Route
r.POST(path string, handlers ...HandlerFunc) *Route
r.PUT(path string, handlers ...HandlerFunc) *Route
r.DELETE(path string, handlers ...HandlerFunc) *Route
r.PATCH(path string, handlers ...HandlerFunc) *Route
r.OPTIONS(path string, handlers ...HandlerFunc) *Route
r.HEAD(path string, handlers ...HandlerFunc) *Route

Example:

r.GET("/users", listUsersHandler)
r.POST("/users", createUserHandler)
r.GET("/users/:id", getUserHandler)

Middleware

r.Use(middleware ...HandlerFunc)

Adds global middleware to the router.

r.Use(Logger(), Recovery())

Route Groups

r.Group(prefix string, middleware ...HandlerFunc) *Group

Creates a new route group with the specified prefix and optional middleware.

api := r.Group("/api/v1")
api.Use(Auth())
api.GET("/users", listUsers)

API Versioning

r.Version(version string) *Group

Creates a version-specific route group.

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

Static Files

r.Static(relativePath, root string)
r.StaticFile(relativePath, filepath string)
r.StaticFS(relativePath string, fs http.FileSystem)

Example:

r.Static("/assets", "./public")
r.StaticFile("/favicon.ico", "./static/favicon.ico")

Route Introspection

r.Routes() []RouteInfo

Returns all registered routes for inspection.

Route

Constraints

Apply validation constraints to route parameters:

route.WhereInt(param string) *Route
route.WhereFloat(param string) *Route
route.WhereUUID(param string) *Route
route.WhereDate(param string) *Route
route.WhereDateTime(param string) *Route
route.WhereEnum(param string, values ...string) *Route
route.WhereRegex(param, pattern string) *Route

Example:

r.GET("/users/:id", getUserHandler).WhereInt("id")
r.GET("/entities/:uuid", getEntityHandler).WhereUUID("uuid")
r.GET("/status/:state", getStatusHandler).WhereEnum("state", "active", "pending")

Group

Route groups support the same methods as Router, with the group’s prefix automatically prepended.

group.GET(path string, handlers ...HandlerFunc) *Route
group.POST(path string, handlers ...HandlerFunc) *Route
group.Use(middleware ...HandlerFunc)
group.Group(prefix string, middleware ...HandlerFunc) *Group

HandlerFunc

type HandlerFunc func(*Context)

Handler function signature for route handlers and middleware.

Example:

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

Next Steps

2 - Router Options

Configuration options for Router initialization.

Router options are passed to router.New() or router.MustNew() to configure the router.

Router Creation

// With error handling
r, err := router.New(opts...)
if err != nil {
    log.Fatalf("Failed to create router: %v", err)
}

// Panics on invalid configuration. Use at startup.
r := router.MustNew(opts...)

Versioning Options

WithVersioning(opts ...version.Option)

Configures API versioning support using functional options from the version package.

import "rivaas.dev/router/version"

r := router.MustNew(
    router.WithVersioning(
        version.WithHeaderDetection("X-API-Version"),
        version.WithDefault("v1"),
    ),
)

With multiple detection strategies:

r := router.MustNew(
    router.WithVersioning(
        version.WithPathDetection("/api/v{version}"),
        version.WithHeaderDetection("X-API-Version"),
        version.WithQueryDetection("v"),
        version.WithDefault("v2"),
        version.WithResponseHeaders(),
        version.WithSunsetEnforcement(),
    ),
)

Diagnostic Options

WithDiagnostics(handler DiagnosticHandler)

Sets a diagnostic handler for informational events like header injection attempts or configuration warnings.

import "log/slog"

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

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

With metrics:

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

Server Options

WithH2C(enable bool)

Enables HTTP/2 Cleartext (h2c) support.

r := router.MustNew(router.WithH2C(true))

WithServerTimeouts(readHeader, read, write, idle time.Duration)

Configures HTTP server timeouts to prevent slowloris attacks and resource exhaustion.

Defaults (if not set):

  • ReadHeaderTimeout: 5s
  • ReadTimeout: 15s
  • WriteTimeout: 30s
  • IdleTimeout: 60s
r := router.MustNew(router.WithServerTimeouts(
    10*time.Second,  // ReadHeaderTimeout
    30*time.Second,  // ReadTimeout
    60*time.Second,  // WriteTimeout
    120*time.Second, // IdleTimeout
))

Performance Options

WithRouteCompilation(enabled bool)

Turns compiled route matching on or off. By default it’s off: the router uses tree traversal, which is fast and works well for most apps. Turn it on when you have a lot of routes (for example hundreds of static routes). Then the router can use pre-compiled lookups and bloom filters to speed things up.

Default: false (tree traversal)

// Default: tree traversal (no need to set anything)
r := router.MustNew()

// Turn on compiled routes for large APIs
r := router.MustNew(router.WithRouteCompilation(true))

WithBloomFilterSize(size uint64)

Sets the bloom filter size when you use compiled routes. Larger sizes reduce false positives.

Default: 1000
Recommended: 2-3x the number of static routes

r := router.MustNew(router.WithBloomFilterSize(2000)) // For ~1000 routes

WithBloomFilterHashFunctions(numFuncs int)

Sets the number of hash functions for bloom filters.

Default: 3
Range: 1-10 (clamped)

r := router.MustNew(router.WithBloomFilterHashFunctions(4))

WithCancellationCheck(enabled bool) / WithoutCancellationCheck()

Controls context cancellation checking in the middleware chain. When enabled (default), the router checks for canceled contexts between handlers.

// Enabled by default
r := router.MustNew(router.WithCancellationCheck(true))

// Disable if you handle cancellation manually
r := router.MustNew(router.WithoutCancellationCheck())

Complete Example

package main

import (
    "log/slog"
    "net/http"
    "os"
    "time"
    
    "rivaas.dev/router"
    "rivaas.dev/router/version"
)

func main() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    
    // Diagnostic handler
    diagHandler := router.DiagnosticHandlerFunc(func(e router.DiagnosticEvent) {
        logger.Warn(e.Message, "kind", e.Kind, "fields", e.Fields)
    })
    
    // Create router with options
    r := router.MustNew(
        // Versioning
        router.WithVersioning(
            version.WithHeaderDetection("API-Version"),
            version.WithDefault("v1"),
        ),
        
        // Server configuration
        router.WithServerTimeouts(
            10*time.Second,
            30*time.Second,
            60*time.Second,
            120*time.Second,
        ),
        
        // Performance tuning
        router.WithBloomFilterSize(2000),
        
        // Diagnostics
        router.WithDiagnostics(diagHandler),
    )
    
    r.GET("/", func(c *router.Context) {
        c.JSON(200, map[string]string{"message": "Hello"})
    })
    
    http.ListenAndServe(":8080", r)
}

Observability Options

import (
    "rivaas.dev/app"
    "rivaas.dev/tracing"
    "rivaas.dev/metrics"
)

application := app.New(
    app.WithServiceName("my-api"),
    app.WithObservability(
        app.WithTracing(tracing.WithSampleRate(0.1)),
        app.WithMetrics(metrics.WithPrometheus()),
        app.WithExcludePaths("/health", "/metrics"),
    ),
)

Next Steps

3 - Context API

Complete reference for Context methods.

The Context provides access to request/response and utility methods.

Request Information

URL Parameters

c.Param(key string) string

Returns URL parameter value from the route path.

// Route: /users/:id
userID := c.Param("id")
c.AllParams() map[string]string

Returns all URL path parameters as a map.

Query Parameters

c.Query(key string) string
c.QueryDefault(key, defaultValue string) string
c.AllQueries() map[string]string
// GET /search?q=golang&page=2
query := c.Query("q")           // "golang"
page := c.QueryDefault("page", "1") // "2"
all := c.AllQueries()           // map[string]string{"q": "golang", "page": "2"}

Form Data

c.FormValue(key string) string
c.FormValueDefault(key, defaultValue string) string

Returns form parameter from POST request body.

// POST with form data
username := c.FormValue("username")
role := c.FormValueDefault("role", "user")

Headers

c.Request.Header.Get(key string) string
c.RequestHeaders() map[string]string
c.ResponseHeaders() map[string]string

Request Binding

Content Type Validation

c.RequireContentType(allowed ...string) bool
c.RequireContentTypeJSON() bool
if !c.RequireContentTypeJSON() {
    return // 415 Unsupported Media Type already sent
}

Streaming

// Stream JSON array items
router.StreamJSONArray[T](c *Context, each func(T) error, maxItems int) error

// Stream NDJSON (newline-delimited JSON)
router.StreamNDJSON[T](c *Context, each func(T) error) error
err := router.StreamJSONArray(c, func(item User) error {
    return processUser(item)
}, 10000) // Max 10k items

Response Methods

JSON Responses

c.JSON(code int, obj any) error
c.IndentedJSON(code int, obj any) error
c.PureJSON(code int, obj any) error      // No HTML escaping
c.SecureJSON(code int, obj any, prefix ...string) error
c.ASCIIJSON(code int, obj any) error     // All non-ASCII escaped

Other Formats

c.YAML(code int, obj any) error
c.String(code int, value string) error
c.Stringf(code int, format string, values ...any) error
c.HTML(code int, html string) error

Binary & Streaming

c.Data(code int, contentType string, data []byte) error
c.DataFromReader(code int, contentLength int64, contentType string, reader io.Reader, extraHeaders map[string]string) error

File Serving

c.ServeFile(filepath string)

Status & No Content

c.Status(code int)
c.NoContent()

Error Responses

c.WriteErrorResponse(status int, message string)
c.NotFound()
c.MethodNotAllowed(allowed []string)

Headers

c.Header(key, value string)

Sets a response header with automatic security sanitization (newlines stripped).

URL Information

c.Hostname() string    // Host without port
c.Port() string        // Port number
c.Scheme() string      // "http" or "https"
c.BaseURL() string     // scheme + host
c.FullURL() string     // Complete URL with query string

Client Information

c.ClientIP() string      // Real client IP (respects trusted proxies)
c.ClientIPs() []string   // All IPs from X-Forwarded-For chain
c.IsHTTPS() bool         // Request over HTTPS
c.IsLocalhost() bool     // Request from localhost
c.IsXHR() bool           // XMLHttpRequest (AJAX)
c.Subdomains(offset ...int) []string

Content Type Detection

c.IsJSON() bool      // Content-Type is application/json
c.IsXML() bool       // Content-Type is application/xml or text/xml
c.AcceptsJSON() bool // Accept header includes application/json
c.AcceptsHTML() bool // Accept header includes text/html

Content Negotiation

c.Accepts(offers ...string) string
c.AcceptsCharsets(offers ...string) string
c.AcceptsEncodings(offers ...string) string
c.AcceptsLanguages(offers ...string) string
// Accept: application/json, text/html;q=0.9
best := c.Accepts("json", "html", "xml") // "json"

// Accept-Language: en-US, fr;q=0.8
lang := c.AcceptsLanguages("en", "fr", "de") // "en"

Caching

c.IsFresh() bool  // Response still fresh in client cache
c.IsStale() bool  // Client cache is stale
if c.IsFresh() {
    c.Status(http.StatusNotModified) // 304
    return
}

Redirects

c.Redirect(code int, location string)
c.Redirect(http.StatusFound, "/login")
c.Redirect(http.StatusMovedPermanently, "https://newdomain.com")

Cookies

c.SetCookie(name, value string, maxAge int, path, domain string, secure, httpOnly bool)
c.GetCookie(name string) (string, error)

File Uploads

c.File(name string) (*File, error)
c.Files(name string) ([]*File, error)

File methods:

file.Bytes() ([]byte, error)
file.Open() (io.ReadCloser, error)
file.Save(dst string) error
file.Ext() string
file, err := c.File("avatar")
if err != nil {
    return c.JSON(400, map[string]string{"error": "avatar required"})
}
file.Save("./uploads/" + uuid.New().String() + file.Ext())

Middleware Control

c.Next()           // Execute next handler in chain
c.Abort()          // Stop handler chain
c.IsAborted() bool // Check if chain was aborted

Error Collection

c.Error(err error)      // Collect error without writing response
c.Errors() []error      // Get all collected errors
c.HasErrors() bool      // Check if errors were collected

Note: router.Context.Error() collects errors without writing a response or aborting the handler chain. This is useful for gathering multiple errors before deciding how to respond.

To send an error response immediately, use app.Context.Fail() which formats the error, writes the response, and stops the handler chain.

if err := validateUser(c); err != nil {
    c.Error(err)
}
if err := validateEmail(c); err != nil {
    c.Error(err)
}

if c.HasErrors() {
    c.JSON(400, map[string]any{"errors": c.Errors()})
    return
}

Context Access

c.RequestContext() context.Context  // Request's context.Context

For tracing and metrics in your handlers, use the app package. The app observability guide shows how to use app.Context methods such as TraceID(), SpanID(), SetSpanAttribute(), AddSpanEvent(), RecordHistogram(), IncrementCounter(), and SetGauge().

Versioning

c.Version() string           // Current API version ("v1", "v2", etc.)
c.IsVersion(version string) bool
c.RoutePattern() string      // Matched route pattern ("/users/:id")

Complete Example

func handler(c *router.Context) {
    // Parameters
    id := c.Param("id")
    query := c.Query("q")
    
    // Headers
    auth := c.Request.Header.Get("Authorization")
    c.Header("X-Custom", "value")
    
    // Strict binding (for full binding, use binding package)
    var req CreateRequest
    if err := c.BindStrict(&req, router.BindOptions{MaxBytes: 1 << 20}); err != nil {
        return // Error response already written
    }
    
    // Logging (pass request context for trace correlation)
    slog.InfoContext(c.RequestContext(), "processing request", "user_id", id)
    
    // Response
    if err := c.JSON(200, map[string]string{
        "id":    id,
        "query": query,
    }); err != nil {
        slog.ErrorContext(c.RequestContext(), "failed to write response", "error", err)
    }
}

Next Steps

4 - Router Performance

Comprehensive benchmark comparison between rivaas/router and other popular Go web frameworks, with methodology and reproduction instructions.

This page contains detailed performance benchmarks comparing rivaas/router against other popular Go web frameworks. The benchmarks measure pure routing dispatch overhead by using direct writes (via io.WriteString) in all handlers to eliminate string concatenation allocations.

Benchmark Methodology

Test Environment

  • Go Version: 1.26
  • CPU: AMD EPYC 7763 64-Core Processor
  • OS: linux/amd64
  • Last Updated: 2026-02-26

Frameworks Compared

The following frameworks are included in the comparison:

Test Scenarios

All frameworks are tested with the same three route patterns:

  1. Static route: GET /
  2. One parameter: GET /users/:id
  3. Two parameters: GET /users/:id/posts/:post_id

Handler Implementation

To ensure fair comparison and isolate routing overhead, all handlers use direct writes rather than string concatenation:

// Instead of this (causes one string allocation):
w.Write([]byte("User: " + id))

// Handlers do this (zero allocations for supported frameworks):
io.WriteString(w, "User: ")
io.WriteString(w, id)

This eliminates the handler allocation cost, so the measured time represents:

  • Route tree traversal and matching
  • Parameter extraction
  • Context setup
  • Response writer overhead (framework-specific)

Measurement Notes

  • Fiber v2/v3: Measured via net/http adaptor (fiberadaptor.FiberApp) for compatibility with httptest.ResponseRecorder. The adaptor adds overhead but is necessary for the standard test harness.
  • Hertz: Measured using ut.PerformRequest(h.Engine, ...) (Hertz’s native test API) because Hertz does not implement http.Handler. Numbers are not directly comparable to httptest-based frameworks due to different measurement approach.
  • Beego: May log “init global config instance failed” when conf/app.conf is missing; this is safe to ignore in benchmarks.

Benchmark Results

Static Route (/)

This scenario measures the overhead of dispatching a request to a static route with no parameters.

Frameworkns/opB/opallocs/opNotes
Rivaas47.400Zero alloc
Gin61.100Zero alloc
Echo78.281
StdMux80.000Zero alloc
Chi347.63682
Beego663.13604
Hertz1720.0344824via ut.PerformRequest
Fiber2034.0206620via http adaptor
FiberV37116.03358215via http adaptor

Scenario: / — Lower is better. All handlers use direct writes (io.WriteString) to minimize overhead and isolate routing cost.

Key Observations:

  • Rivaas, Gin, and StdMux achieve zero allocations with direct writes
  • Echo has 1 allocation from its internal context
  • Chi, Fiber, Hertz, and Beego have framework-specific overhead

One Parameter (/users/:id)

This scenario measures routing + parameter extraction for a single dynamic segment.

Frameworkns/opB/opallocs/opNotes
Rivaas82.200Zero alloc
Gin104.400Zero alloc
Echo149.6162
StdMux212.2161
Chi407.23682
Beego1017.04006
Hertz2035.0354427via ut.PerformRequest
Fiber2156.0206020via http adaptor
FiberV37410.03311216via http adaptor

Scenario: /users/:id — Lower is better. All handlers use direct writes (io.WriteString) to minimize overhead and isolate routing cost.

Key Observations:

  • Rivaas and Gin maintain zero allocations even with parameter extraction
  • StdMux has 1 allocation from r.PathValue()
  • Echo has 2 allocations (context + param storage)

Two Parameters (/users/:id/posts/:post_id)

This scenario tests routing with multiple dynamic segments.

Frameworkns/opB/opallocs/opNotes
Rivaas130.900Zero alloc
Gin165.200Zero alloc
Echo251.3324
StdMux350.3482
Chi507.33682
Beego1362.04488
Hertz2160.0366429via ut.PerformRequest
Fiber2346.0207720via http adaptor
FiberV37403.03312818via http adaptor

Scenario: /users/:id/posts/:post_id — Lower is better. All handlers use direct writes (io.WriteString) to minimize overhead and isolate routing cost.

Key Observations:

  • Rivaas and Gin continue to show zero allocations
  • StdMux scales linearly (2 allocs for 2 params)
  • Echo scales with each additional parameter

How to Reproduce

The benchmarks are located in the router/benchmarks directory of the rivaas repository.

Running All Benchmarks

cd router/benchmarks
go test -bench=. -benchmem

Running a Specific Scenario

# Static route only
go test -bench=BenchmarkStatic -benchmem

# One parameter only
go test -bench=BenchmarkOneParam -benchmem

# Two parameters only
go test -bench=BenchmarkTwoParams -benchmem

Running a Specific Framework

# Rivaas only
go test -bench='/(Rivaas)$' -benchmem

# Gin only
go test -bench='/(Gin)$' -benchmem

Multiple Runs for Statistical Analysis

Use -count to run benchmarks multiple times and benchstat to compare:

go test -bench=. -benchmem -count=5 > results.txt
go install golang.org/x/perf/cmd/benchstat@latest
benchstat results.txt

Understanding the Results

Metrics Explained

  • ns/op: Nanoseconds per operation (lower is better)
  • B/op: Bytes allocated per operation (lower is better)
  • allocs/op: Number of allocations per operation (lower is better)

Why Zero Allocations Matter

The router is zero allocation for the benchmarked scenarios: static route, one parameter, and two parameters.

Each allocation has a cost:

  • Time: Allocating memory takes time (~30-50ns for small allocations)
  • GC pressure: More allocations mean more garbage collection work
  • Scalability: At high request rates (millions/sec), eliminating allocations significantly reduces CPU and memory usage

Rivaas achieves zero allocations for routing and parameter extraction by:

  • Pre-allocating context pools
  • Using array-based parameter storage for ≤8 params
  • Avoiding string concatenation in hot paths
  • Efficient radix tree implementation with minimal allocations

Continuous Benchmarking

The rivaas repository uses continuous benchmarking to detect performance regressions:

  • Pull Requests: Every PR runs Rivaas-only benchmarks and compares against a baseline. If performance regresses beyond a threshold, the PR check fails.
  • Releases: Full framework comparison runs on every release tag and updates this page automatically.

See the benchmarks.yml workflow for implementation details.


See Also

5 - Route Constraints

Type-safe parameter validation with route constraints.

Route constraints provide parameter validation that maps to OpenAPI schema types.

Typed Constraints

WhereInt(param string) *Route

Validates parameter as integer (OpenAPI: type: integer, format: int64).

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

Matches:

  • /users/123
  • /users/abc

WhereFloat(param string) *Route

Validates parameter as float (OpenAPI: type: number, format: double).

r.GET("/prices/:amount", getPriceHandler).WhereFloat("amount")

Matches:

  • /prices/19.99
  • /prices/abc

WhereUUID(param string) *Route

Validates parameter as UUID (OpenAPI: type: string, format: uuid).

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

Matches:

  • /entities/550e8400-e29b-41d4-a716-446655440000
  • /entities/not-a-uuid

WhereDate(param string) *Route

Validates parameter as date (OpenAPI: type: string, format: date).

r.GET("/orders/:date", getOrderHandler).WhereDate("date")

Matches:

  • /orders/2024-01-18
  • /orders/invalid-date

WhereDateTime(param string) *Route

Validates parameter as date-time (OpenAPI: type: string, format: date-time).

r.GET("/events/:timestamp", getEventHandler).WhereDateTime("timestamp")

Matches:

  • /events/2024-01-18T10:30:00Z
  • /events/invalid

WhereEnum(param string, values ...string) *Route

Validates parameter against enum values (OpenAPI: enum).

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

Matches:

  • /status/active
  • /status/invalid

Regex Constraints

WhereRegex(param, pattern string) *Route

Custom regex validation (OpenAPI: pattern).

// Alphanumeric only
r.GET("/slugs/:slug", getSlugHandler).WhereRegex("slug", `[a-zA-Z0-9]+`)

// Email validation
r.GET("/users/:email", getUserByEmailHandler).WhereRegex("email", `[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}`)

Multiple Constraints

Apply multiple constraints to the same route:

r.GET("/posts/:id/:slug", getPostHandler).
    WhereInt("id").
    WhereRegex("slug", `[a-zA-Z0-9-]+`)

Common Patterns

RESTful IDs

// Integer IDs
r.GET("/users/:id", getUserHandler).WhereInt("id")
r.PUT("/users/:id", updateUserHandler).WhereInt("id")
r.DELETE("/users/:id", deleteUserHandler).WhereInt("id")

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

Slugs and Identifiers

// Alphanumeric slugs
r.GET("/posts/:slug", getPostBySlugHandler).WhereRegex("slug", `[a-z0-9-]+`)

// Category identifiers
r.GET("/categories/:name", getCategoryHandler).WhereRegex("name", `[a-zA-Z0-9_-]+`)

Status and States

// Enum validation for states
r.GET("/orders/:status", getOrdersByStatusHandler).WhereEnum("status", "pending", "processing", "shipped", "delivered")

Complete Example

package main

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

func main() {
    r := router.New()
    
    // Integer constraint
    r.GET("/users/:id", getUserHandler).WhereInt("id")
    
    // UUID constraint
    r.GET("/entities/:uuid", getEntityHandler).WhereUUID("uuid")
    
    // Enum constraint
    r.GET("/status/:state", getStatusHandler).WhereEnum("state", "active", "inactive", "pending")
    
    // Regex constraint
    r.GET("/posts/:slug", getPostHandler).WhereRegex("slug", `[a-z0-9-]+`)
    
    // Multiple constraints
    r.GET("/articles/:id/:slug", getArticleHandler).
        WhereInt("id").
        WhereRegex("slug", `[a-z0-9-]+`)
    
    http.ListenAndServe(":8080", r)
}

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

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

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

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

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

Next Steps

6 - Middleware Reference

Built-in middleware catalog with configuration options.

The router includes production-ready middleware in separate packages. Each middleware is its own Go module, so you only add the ones you need and keep your dependency footprint small. All of them use functional options for configuration.

Security

Security Headers

Package: rivaas.dev/middleware/security

go get rivaas.dev/middleware/security
import "rivaas.dev/middleware/security"

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

CORS

Package: rivaas.dev/middleware/cors

go get rivaas.dev/middleware/cors
import "rivaas.dev/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),
    cors.WithMaxAge(3600),
))

Basic Auth

Package: rivaas.dev/middleware/basicauth

go get rivaas.dev/middleware/basicauth
import "rivaas.dev/middleware/basicauth"

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

Observability

Access Log

Package: rivaas.dev/middleware/accesslog

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

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

Request ID

Package: rivaas.dev/middleware/requestid

go get rivaas.dev/middleware/requestid

Generates unique, time-ordered request IDs for distributed tracing and log correlation.

import "rivaas.dev/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")))

// Get request ID in handlers
func handler(c *router.Context) {
    id := requestid.Get(c)
}

ID Formats:

  • UUID v7 (default): 018f3e9a-1b2c-7def-8000-abcdef123456
  • ULID: 01ARZ3NDEKTSV4RRFFQ69G5FAV

Reliability

Recovery

Package: rivaas.dev/middleware/recovery

go get rivaas.dev/middleware/recovery
import "rivaas.dev/middleware/recovery"

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

Timeout

Package: rivaas.dev/middleware/timeout

go get rivaas.dev/middleware/timeout
import "rivaas.dev/middleware/timeout"

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

Rate Limit

Package: rivaas.dev/middleware/ratelimit

go get rivaas.dev/middleware/ratelimit
import "rivaas.dev/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
    }),
    ratelimit.WithLogger(logger),
))

Body Limit

Package: rivaas.dev/middleware/bodylimit

go get rivaas.dev/middleware/bodylimit
import "rivaas.dev/middleware/bodylimit"

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

Performance

Compression

Package: rivaas.dev/middleware/compression

go get rivaas.dev/middleware/compression
import "rivaas.dev/middleware/compression"

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

Other

Method Override

Package: rivaas.dev/middleware/methodoverride

go get rivaas.dev/middleware/methodoverride
import "rivaas.dev/middleware/methodoverride"

r.Use(methodoverride.New(
    methodoverride.WithHeader("X-HTTP-Method-Override"),
))

Trailing Slash

Package: rivaas.dev/middleware/trailingslash

go get rivaas.dev/middleware/trailingslash
import "rivaas.dev/middleware/trailingslash"

r.Use(trailingslash.New(
    trailingslash.WithRedirectCode(301),
))

Middleware Ordering

Recommended middleware order:

r := router.New()

// 1. Request ID
r.Use(requestid.New())

// 2. AccessLog
r.Use(accesslog.New())

// 3. Recovery
r.Use(recovery.New())

// 4. Security/CORS
r.Use(security.New())
r.Use(cors.New())

// 5. Body Limit
r.Use(bodylimit.New())

// 6. Rate Limit
r.Use(ratelimit.New())

// 7. Timeout
r.Use(timeout.New())

// 8. Authentication
r.Use(auth.New())

// 9. Compression (last)
r.Use(compression.New())

Complete Example

package main

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

func main() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    r := router.New()
    
    // Observability
    r.Use(requestid.New())
    r.Use(accesslog.New(
        accesslog.WithLogger(logger),
        accesslog.WithExcludePaths("/health"),
    ))
    
    // Reliability
    r.Use(recovery.New())
    
    // Security
    r.Use(security.New())
    r.Use(cors.New(
        cors.WithAllowedOrigins("*"),
        cors.WithAllowedMethods("GET", "POST", "PUT", "DELETE"),
    ))
    
    r.GET("/", func(c *router.Context) {
        c.JSON(200, map[string]string{"message": "Hello"})
    })
    
    http.ListenAndServe(":8080", r)
}

Next Steps

7 - Diagnostics

Diagnostic event types and handling.

The router emits optional diagnostic events for security concerns and configuration issues.

Event Types

DiagXFFSuspicious

Suspicious X-Forwarded-For chain detected (>10 IPs).

Fields:

  • chain (string) - The full X-Forwarded-For header value
  • count (int) - Number of IPs in the chain
handler := router.DiagnosticHandlerFunc(func(e router.DiagnosticEvent) {
    if e.Kind == router.DiagXFFSuspicious {
        log.Printf("Suspicious XFF chain: %s (count: %d)", 
            e.Fields["chain"], e.Fields["count"])
    }
})

DiagHeaderInjection

Header injection attempt blocked and sanitized.

Fields:

  • header (string) - Header name
  • value (string) - Original value
  • sanitized (string) - Sanitized value

DiagInvalidProto

Invalid X-Forwarded-Proto value.

Fields:

  • proto (string) - Invalid protocol value

DiagHighParamCount

Route has >8 parameters (uses map storage instead of array).

Fields:

  • method (string) - HTTP method
  • path (string) - Route path
  • param_count (int) - Number of parameters

DiagH2CEnabled

H2C enabled (development warning).

Fields:

  • None

Enabling Diagnostics

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))

Handler Examples

With Logging

import "log/slog"

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

With Metrics

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

With OpenTelemetry

import (
    "go.opentelemetry.io/otel/attribute"
    "go.opentelemetry.io/otel/trace"
)

handler := router.DiagnosticHandlerFunc(func(e router.DiagnosticEvent) {
    span := trace.SpanFromContext(ctx)
    if span.IsRecording() {
        attrs := []attribute.KeyValue{
            attribute.String("diagnostic.kind", string(e.Kind)),
        }
        for k, v := range e.Fields {
            attrs = append(attrs, attribute.String(k, fmt.Sprint(v)))
        }
        span.AddEvent(e.Message, trace.WithAttributes(attrs...))
    }
})

Complete Example

package main

import (
    "log/slog"
    "net/http"
    "os"
    
    "rivaas.dev/router"
)

func main() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    
    // Diagnostic handler
    diagHandler := router.DiagnosticHandlerFunc(func(e router.DiagnosticEvent) {
        logger.Warn(e.Message,
            "kind", e.Kind,
            "fields", e.Fields,
        )
    })
    
    // Create router with diagnostics
    r := router.New(router.WithDiagnostics(diagHandler))
    
    r.GET("/", func(c *router.Context) {
        c.JSON(200, map[string]string{"message": "Hello"})
    })
    
    http.ListenAndServe(":8080", r)
}

Best Practices

  1. Log diagnostic events for security monitoring
  2. Track metrics for diagnostic event frequency
  3. Alert on suspicious patterns (e.g., repeated XFF warnings)
  4. Don’t ignore warnings - they indicate potential issues

Next Steps

8 - Troubleshooting

Common issues and solutions for the router package.

This guide helps you troubleshoot common issues with the Rivaas Router.

Quick Reference

IssueSolutionExample
404 Route Not FoundCheck route syntax and order.r.GET("/users/:id", handler)
Middleware Not RunningRegister before routes.r.Use(middleware); r.GET("/path", handler)
Parameters Not WorkingUse :param syntax.r.GET("/users/:id", handler)
CORS IssuesAdd CORS middleware.r.Use(cors.New())
Memory LeaksDon’t store context references.Extract data immediately.
Slow PerformanceUse route groups.api := r.Group("/api")

Common Issues

Route Not Found (404 errors)

Problem: Routes not matching as expected.

Solutions:

// ✅ Correct: Use :param syntax
r.GET("/users/:id", handler)

// ❌ Wrong: Don't use {param} syntax
r.GET("/users/{id}", handler)

// ✅ Correct: Static route
r.GET("/users/me", currentUserHandler)

// Check route registration order
r.GET("/users/me", currentUserHandler)      // Register specific routes first
r.GET("/users/:id", getUserHandler)         // Then parameter routes

Middleware Not Executing

Problem: Middleware doesn’t run for routes.

Solution: Register middleware before routes.

// ✅ Correct: Middleware before routes
r.Use(Logger())
r.GET("/api/users", handler)

// ❌ Wrong: Routes before middleware
r.GET("/api/users", handler)
r.Use(Logger()) // Too late!

// ✅ Correct: Group middleware
api := r.Group("/api")
api.Use(Auth())
api.GET("/users", handler)

Parameter Constraints Not Working

Problem: Invalid parameters still match routes.

Solution: Apply constraints to routes.

// ✅ Correct: Integer constraint
r.GET("/users/:id", handler).WhereInt("id")

// ✅ Correct: Custom regex
r.GET("/files/:name", handler).WhereRegex("name", `[a-zA-Z0-9.-]+`)

// ❌ Wrong: No constraint (matches anything)
r.GET("/users/:id", handler) // Matches "/users/abc"

Memory Leaks

Problem: Growing memory usage.

Solution: Never store Context references.

// ❌ Wrong: Storing context
var globalContext *router.Context
func handler(c *router.Context) {
    globalContext = c // Memory leak!
}

// ✅ Correct: Extract data immediately
func handler(c *router.Context) {
    userID := c.Param("id")
    // Use userID, not c
    processUser(userID)
}

// ✅ Correct: Copy data for async operations
func handler(c *router.Context) {
    userID := c.Param("id")
    go func(id string) {
        processAsync(id)
    }(userID)
}

CORS Issues

Problem: CORS errors in browser.

Solution: Add CORS middleware.

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"),
))

Slow Performance

Problem: Routes are slow.

Solutions:

// ✅ Use route groups
api := r.Group("/api")
api.GET("/users", handler)
api.GET("/posts", handler)

// ✅ Minimize middleware
r.Use(Recovery()) // Essential only

// ✅ Apply constraints for parameter validation
r.GET("/users/:id", handler).WhereInt("id")

// ❌ Don't parse parameters manually
func handler(c *router.Context) {
    // id, err := strconv.Atoi(c.Param("id")) // Slow
    id := c.Param("id") // Fast
}

Validation Errors

Problem: Validation not working.

Solutions:

// ✅ Register custom tags in init()
func init() {
    router.RegisterTag("custom", validatorFunc)
}

// ✅ Use app.Context for binding and validation
func createUser(c *app.Context) {
    var req CreateUserRequest
    if !c.MustBind(&req) {
        return
    }
}

// ✅ Partial validation for PATCH
func updateUser(c *app.Context) {
    req, ok := app.MustBindPatch[UpdateUserRequest](c)
    if !ok {
        return
    }
}

FAQ

Can I use standard HTTP middleware?

Yes! Adapt existing middleware:

func adaptMiddleware(next http.Handler) router.HandlerFunc {
    return func(c *router.Context) {
        next.ServeHTTP(c.Writer, c.Request)
    }
}

Is the router production-ready?

Yes. The router is production-ready with:

  • 84.8% code coverage
  • Comprehensive test suite
  • Zero race conditions
  • Zero allocation for routing and param extraction in typical use (≤8 path params)
  • High throughput (see Performance for current numbers)

How do I handle CORS?

Use the built-in CORS middleware:

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

r.Use(cors.New(
    cors.WithAllowedOrigins("*"),
    cors.WithAllowedMethods("GET", "POST", "PUT", "DELETE"),
))

Why are my parameters not working?

Check the parameter syntax:

// ✅ Correct
r.GET("/users/:id", handler)
id := c.Param("id")

// ❌ Wrong syntax
r.GET("/users/{id}", handler) // Use :id instead

How do I debug routing issues?

Use route introspection:

routes := r.Routes()
for _, route := range routes {
    fmt.Printf("%s %s -> %s\n", route.Method, route.Path, route.HandlerName)
}

Getting Help

Next Steps