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
- 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
External Links
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
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.
Security Warning
Only use in development or behind a trusted load balancer. DO NOT enable on public-facing servers without TLS.
r := router.MustNew(router.WithH2C(true))
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
))
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
Note
For tracing, metrics, and logging configuration, use the app package which provides WithObservability(), WithTracing(), WithMetrics(), and WithLogging() options. These options configure the full observability stack and integrate with the router automatically.
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.
Memory Safety
Context objects are pooled and reused. Never store references to Context beyond the request handler. Check Context Guide for details.
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"}
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")
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
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)
c.Header(key, value string)
Sets a response header with automatic security sanitization (newlines stripped).
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
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:
- Static route:
GET / - One parameter:
GET /users/:id - 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.
| Framework | ns/op | B/op | allocs/op | Notes |
|---|
| Rivaas | 47.4 | 0 | 0 | Zero alloc |
| Gin | 61.1 | 0 | 0 | Zero alloc |
| Echo | 78.2 | 8 | 1 | |
| StdMux | 80.0 | 0 | 0 | Zero alloc |
| Chi | 347.6 | 368 | 2 | |
| Beego | 663.1 | 360 | 4 | |
| Hertz | 1720.0 | 3448 | 24 | via ut.PerformRequest |
| Fiber | 2034.0 | 2066 | 20 | via http adaptor |
| FiberV3 | 7116.0 | 33582 | 15 | via 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.
| Framework | ns/op | B/op | allocs/op | Notes |
|---|
| Rivaas | 82.2 | 0 | 0 | Zero alloc |
| Gin | 104.4 | 0 | 0 | Zero alloc |
| Echo | 149.6 | 16 | 2 | |
| StdMux | 212.2 | 16 | 1 | |
| Chi | 407.2 | 368 | 2 | |
| Beego | 1017.0 | 400 | 6 | |
| Hertz | 2035.0 | 3544 | 27 | via ut.PerformRequest |
| Fiber | 2156.0 | 2060 | 20 | via http adaptor |
| FiberV3 | 7410.0 | 33112 | 16 | via 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.
| Framework | ns/op | B/op | allocs/op | Notes |
|---|
| Rivaas | 130.9 | 0 | 0 | Zero alloc |
| Gin | 165.2 | 0 | 0 | Zero alloc |
| Echo | 251.3 | 32 | 4 | |
| StdMux | 350.3 | 48 | 2 | |
| Chi | 507.3 | 368 | 2 | |
| Beego | 1362.0 | 448 | 8 | |
| Hertz | 2160.0 | 3664 | 29 | via ut.PerformRequest |
| Fiber | 2346.0 | 2077 | 20 | via http adaptor |
| FiberV3 | 7403.0 | 33128 | 18 | via 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:
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
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
))
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 valuecount (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"])
}
})
Header injection attempt blocked and sanitized.
Fields:
header (string) - Header namevalue (string) - Original valuesanitized (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 methodpath (string) - Route pathparam_count (int) - Number of parameters
DiagH2CEnabled
H2C enabled (development warning).
Fields:
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
- Log diagnostic events for security monitoring
- Track metrics for diagnostic event frequency
- Alert on suspicious patterns (e.g., repeated XFF warnings)
- 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
| Issue | Solution | Example |
|---|
| 404 Route Not Found | Check route syntax and order. | r.GET("/users/:id", handler) |
| Middleware Not Running | Register before routes. | r.Use(middleware); r.GET("/path", handler) |
| Parameters Not Working | Use :param syntax. | r.GET("/users/:id", handler) |
| CORS Issues | Add CORS middleware. | r.Use(cors.New()) |
| Memory Leaks | Don’t store context references. | Extract data immediately. |
| Slow Performance | Use 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"),
))
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