HTTP Router
Type-safe HTTP routing with request tracing, panic recovery, and Context-based API in Velocity.
The Velocity HTTP router provides a modern, type-safe routing system with built-in request tracing, automatic panic recovery, and an ergonomic Context-based API inspired by Fiber and Echo.
Quick Start
package main
import (
"net/http"
"github.com/velocitykode/velocity/pkg/router"
)
func main() {
r := router.Get()
r.Get("/users/{id}", func(ctx *router.Context) error {
id := ctx.Param("id")
return ctx.JSON(200, map[string]interface{}{
"id": id,
"name": "John Doe",
})
})
http.ListenAndServe(":4000", r)
}Core Features
Context-Based Handlers
All route handlers receive a *router.Context object that provides access to the request, response, and helper methods:
r.Get("/api/user", func(ctx *router.Context) error {
// Access request data
userAgent := ctx.Request.Header.Get("User-Agent")
// Set response headers
ctx.Response.Header().Set("X-Custom-Header", "value")
// Return JSON response
return ctx.JSON(200, map[string]string{
"message": "Hello, World!",
})
})HTTP Methods
The router supports all standard HTTP methods:
r.Get("/users", listUsers)
r.Post("/users", createUser)
r.Put("/users/{id}", updateUser)
r.Patch("/users/{id}", patchUser)
r.Delete("/users/{id}", deleteUser)Route Parameters
Extract URL parameters using ctx.Param():
r.Get("/users/{id}", func(ctx *router.Context) error {
// Get string parameter
id := ctx.Param("id")
return ctx.JSON(200, map[string]interface{}{
"id": id,
})
})Query Parameters
Extract query parameters using ctx.Query():
r.Get("/search", func(ctx *router.Context) error {
// String query parameter
query := ctx.Query("q")
// Get all query parameters via Request
page := ctx.Request.URL.Query().Get("page")
return ctx.JSON(200, map[string]interface{}{
"query": query,
"page": page,
})
})Request Handling
JSON Binding
Parse JSON request bodies into structs using ctx.Bind():
type CreateUserRequest struct {
Name string `json:"name"`
Email string `json:"email"`
Age int `json:"age"`
}
r.Post("/users", func(ctx *router.Context) error {
var req CreateUserRequest
if err := ctx.Bind(&req); err != nil {
return ctx.JSON(400, map[string]string{"error": "Invalid request body"})
}
// Use req.Name, req.Email, req.Age
return ctx.JSON(201, req)
})Form Data
Access form data via the underlying Request:
r.Post("/contact", func(ctx *router.Context) error {
// Parse form
if err := ctx.Request.ParseForm(); err != nil {
return ctx.JSON(400, map[string]string{"error": "Invalid form"})
}
name := ctx.Request.FormValue("name")
email := ctx.Request.FormValue("email")
return ctx.JSON(200, map[string]string{
"name": name,
"email": email,
})
})Response Handling
JSON Responses
r.Get("/api/data", func(ctx *router.Context) error {
data := map[string]interface{}{
"status": "success",
"data": []string{"item1", "item2"},
}
return ctx.JSON(200, data)
})Redirects
Use ctx.Redirect() for HTTP redirects:
r.Get("/old-path", func(ctx *router.Context) error {
return ctx.Redirect(301, "/new-path")
})
r.Post("/login", func(ctx *router.Context) error {
// After successful login
return ctx.Redirect(303, "/dashboard")
})Raw Responses
Access the underlying http.ResponseWriter for full control:
r.Get("/download", func(ctx *router.Context) error {
ctx.Response.Header().Set("Content-Type", "application/octet-stream")
ctx.Response.Header().Set("Content-Disposition", "attachment; filename=file.txt")
ctx.Response.Write([]byte("file contents"))
return nil
})Request Tracing
The router automatically extracts trace and request IDs from headers:
router.GET("/api/log", func(ctx *velocity.Ctx) error {
log.Info("Processing request",
"trace_id", ctx.TraceID, // From X-Trace-ID header
"request_id", ctx.RequestID, // From X-Request-ID header
)
return ctx.JSON(200, map[string]string{
"trace_id": ctx.TraceID,
"request_id": ctx.RequestID,
})
})Clients should send these headers:
curl -H "X-Trace-ID: trace-123" \
-H "X-Request-ID: req-456" \
http://localhost:4000/api/logRequest-Scoped Storage
Store and retrieve data within a request’s lifecycle using Locals:
// Middleware sets user data
func AuthMiddleware(next velocity.HandlerFunc) velocity.HandlerFunc {
return func(ctx *velocity.Ctx) error {
// Authenticate user...
ctx.SetLocal("user_id", 123)
ctx.SetLocal("username", "john")
return next(ctx)
}
}
// Handler retrieves it
router.GET("/profile", func(ctx *velocity.Ctx) error {
userID := ctx.Locals("user_id").(int)
username := ctx.Locals("username").(string)
return ctx.JSON(200, map[string]interface{}{
"user_id": userID,
"username": username,
})
})Error Handling
Predefined Errors
Use built-in error types for common HTTP errors:
router.GET("/users/{id}", func(ctx *velocity.Ctx) error {
id, err := ctx.ParamInt("id")
if err != nil {
return velocity.ErrBadRequest // 400
}
user := findUser(id)
if user == nil {
return velocity.ErrNotFound // 404
}
if !hasPermission(ctx) {
return velocity.ErrForbidden // 403
}
return ctx.JSON(200, user)
})Available predefined errors:
ErrBadRequest(400)ErrUnauthorized(401)ErrForbidden(403)ErrNotFound(404)ErrMethodNotAllowed(405)ErrInternalServerError(500)ErrBadGateway(502)ErrServiceUnavailable(503)
Custom Errors
Create custom error responses:
router.POST("/users", func(ctx *velocity.Ctx) error {
var user User
if err := ctx.BindJSON(&user); err != nil {
return &velocity.Error{
Code: 422,
Message: "Validation failed: " + err.Error(),
}
}
return ctx.JSON(201, user)
})Error Helper Methods
router.GET("/api/data", func(ctx *velocity.Ctx) error {
if !authorized {
return ctx.Unauthorized("Please log in")
}
if !hasAccess {
return ctx.Forbidden("Access denied")
}
if invalidInput {
return ctx.BadRequest("Invalid input")
}
if serverError {
return ctx.InternalServerError("Server error")
}
return ctx.JSON(200, data)
})Custom Error Handler
Override the default error handler:
func CustomErrorHandler(ctx *velocity.Ctx, err error) {
code := 500
message := "Internal Server Error"
if e, ok := err.(*velocity.Error); ok {
code = e.Code
message = e.Message
}
// Log error
log.Error("Request error",
"error", err,
"trace_id", ctx.TraceID,
"error_id", ctx.ErrorID,
)
// Custom response format
ctx.JSON(code, map[string]interface{}{
"success": false,
"error": message,
"error_id": ctx.ErrorID,
"timestamp": time.Now().Unix(),
})
}
router := velocity.NewRouter(velocity.Config{
ErrorHandler: CustomErrorHandler,
})Automatic Panic Recovery
The router automatically recovers from panics and converts them to errors:
router.GET("/panic", func(ctx *velocity.Ctx) error {
panic("something went wrong")
// Automatically caught and returned as 500 error
})Middleware
Route-Specific Middleware
Apply middleware to specific routes:
func LoggingMiddleware(next velocity.HandlerFunc) velocity.HandlerFunc {
return func(ctx *velocity.Ctx) error {
start := time.Now()
err := next(ctx)
log.Info("Request completed",
"path", ctx.Request.URL.Path,
"duration", time.Since(start),
)
return err
}
}
router.GET("/api/users", getUsers).
Middleware(LoggingMiddleware)Global Middleware
Apply middleware to all routes:
router.Middleware(LoggingMiddleware, AuthMiddleware)
router.GET("/api/users", getUsers)
router.GET("/api/posts", getPosts)
// Both routes use LoggingMiddleware and AuthMiddlewareMiddleware Order
Middleware executes in this order:
- Global middleware (outer → inner)
- Group middleware (outer → inner)
- Route middleware (outer → inner)
- Handler
// Request flow:
// GlobalMiddleware1 → GlobalMiddleware2 →
// GroupMiddleware → RouteMiddleware → HandlerAuthentication Middleware Example
func AuthMiddleware(next velocity.HandlerFunc) velocity.HandlerFunc {
return func(ctx *velocity.Ctx) error {
token := ctx.Get("Authorization")
if token == "" {
return ctx.Unauthorized("Missing token")
}
user, err := validateToken(token)
if err != nil {
return ctx.Unauthorized("Invalid token")
}
ctx.SetLocal("user", user)
return next(ctx)
}
}
router.GET("/api/profile", getProfile).
Middleware(AuthMiddleware)Route Groups
Group related routes with shared prefixes and middleware:
// API v1 routes
api := router.Prefix("/api/v1")
api.GET("/users", listUsers)
api.POST("/users", createUser)
api.GET("/users/{id}", getUser)
// Admin routes with auth middleware
admin := router.Prefix("/admin")
admin.Middleware(AdminAuthMiddleware)
admin.GET("/dashboard", adminDashboard)
admin.GET("/users", adminUsers)
admin.POST("/settings", updateSettings)
// Nested groups
v2 := router.Prefix("/api/v2")
usersGroup := v2.Prefix("/users")
usersGroup.GET("/", listUsers)
usersGroup.POST("/", createUser)
usersGroup.GET("/{id}", getUser)Named Routes
Name routes for URL generation:
router.GET("/users/{id}", getUser).Name("user.show")
router.POST("/users", createUser).Name("user.create")
router.PUT("/users/{id}", updateUser).Name("user.update")
// Generate URLs
url, err := router.URL("user.show", map[string]string{
"id": "123",
})
// url = "/users/123"Header Manipulation
Reading Headers
router.GET("/api/check", func(ctx *velocity.Ctx) error {
userAgent := ctx.Get("User-Agent")
accept := ctx.Get("Accept")
contentType := ctx.Get("Content-Type")
return ctx.JSON(200, map[string]string{
"user_agent": userAgent,
"accept": accept,
"content_type": contentType,
})
})Setting Headers
router.GET("/api/data", func(ctx *velocity.Ctx) error {
ctx.Set("X-API-Version", "1.0")
ctx.Set("X-Rate-Limit", "100")
ctx.Set("Cache-Control", "no-cache")
return ctx.JSON(200, data)
})Integration with Standard Middleware
Wrap standard http.Handler middleware for use with the router:
import "github.com/rs/cors"
// Standard CORS middleware
corsMiddleware := cors.New(cors.Options{
AllowedOrigins: []string{"*"},
}).Handler
// Wrap it for use with Velocity router
router.UseStandardMiddleware(corsMiddleware)Complete Example
package main
import (
"net/http"
"time"
github.com/velocitykode/velocity
"github.com/velocitykode/velocity/pkg/log"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
func LoggingMiddleware(next velocity.HandlerFunc) velocity.HandlerFunc {
return func(ctx *velocity.Ctx) error {
start := time.Now()
log.Info("Request started",
"method", ctx.Request.Method,
"path", ctx.Request.URL.Path,
"trace_id", ctx.TraceID,
)
err := next(ctx)
log.Info("Request completed",
"duration", time.Since(start),
"trace_id", ctx.TraceID,
)
return err
}
}
func AuthMiddleware(next velocity.HandlerFunc) velocity.HandlerFunc {
return func(ctx *velocity.Ctx) error {
token := ctx.Get("Authorization")
if token == "" {
return ctx.Unauthorized("Missing authorization token")
}
// Validate token and set user
ctx.SetLocal("user_id", 1)
return next(ctx)
}
}
func main() {
router := velocity.NewRouter()
// Global middleware
router.Middleware(LoggingMiddleware)
// Public routes
router.GET("/", func(ctx *velocity.Ctx) error {
return ctx.JSON(200, map[string]string{
"message": "Welcome to the API",
})
})
// API routes
api := router.Prefix("/api")
// Public API endpoints
api.GET("/status", func(ctx *velocity.Ctx) error {
return ctx.JSON(200, map[string]string{
"status": "ok",
"version": "1.0.0",
})
})
// Protected API endpoints
users := api.Prefix("/users")
users.Middleware(AuthMiddleware)
users.GET("/", func(ctx *velocity.Ctx) error {
page := ctx.QueryInt("page", 1)
limit := ctx.QueryInt("limit", 10)
// Fetch users...
users := []User{
{ID: 1, Name: "Alice", Email: "alice@example.com"},
{ID: 2, Name: "Bob", Email: "bob@example.com"},
}
return ctx.JSON(200, map[string]interface{}{
"users": users,
"page": page,
"limit": limit,
})
})
users.GET("/{id}", func(ctx *velocity.Ctx) error {
id, err := ctx.ParamInt("id")
if err != nil {
return ctx.BadRequest("Invalid user ID")
}
// Fetch user...
user := User{
ID: id,
Name: "Alice",
Email: "alice@example.com",
}
return ctx.JSON(200, user)
})
users.POST("/", func(ctx *velocity.Ctx) error {
var user User
if err := ctx.BindJSON(&user); err != nil {
return ctx.BadRequest("Invalid request body")
}
// Create user...
user.ID = 3
return ctx.Status(201).JSON(user)
})
log.Info("Server starting on :4000")
http.ListenAndServe(":4000", router)
}Testing Routes
package main
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
github.com/velocitykode/velocity
)
func TestGetUser(t *testing.T) {
router := velocity.NewRouter()
router.GET("/users/{id}", func(ctx *velocity.Ctx) error {
id, _ := ctx.ParamInt("id")
return ctx.JSON(200, map[string]int{"id": id})
})
req := httptest.NewRequest("GET", "/users/123", nil)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != 200 {
t.Errorf("Expected status 200, got %d", rec.Code)
}
expected := `{"id":123}`
if strings.TrimSpace(rec.Body.String()) != expected {
t.Errorf("Expected %s, got %s", expected, rec.Body.String())
}
}
func TestJSONBinding(t *testing.T) {
router := velocity.NewRouter()
router.POST("/users", func(ctx *velocity.Ctx) error {
var user struct {
Name string `json:"name"`
}
if err := ctx.BindJSON(&user); err != nil {
return velocity.ErrBadRequest
}
return ctx.JSON(201, user)
})
body := strings.NewReader(`{"name":"Alice"}`)
req := httptest.NewRequest("POST", "/users", body)
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != 201 {
t.Errorf("Expected status 201, got %d", rec.Code)
}
}Best Practices
- Use Type-Safe Helpers: Prefer
ParamInt(),QueryInt(),QueryBool()over manual parsing - Return Errors: Always return errors from handlers instead of writing responses directly
- Use Predefined Errors: Use
ErrBadRequest,ErrNotFound, etc. for consistency - Leverage Request Tracing: Include
TraceIDandRequestIDin logs for debugging - Store Request Data in Locals: Use
SetLocal()to pass data between middleware and handlers - Name Important Routes: Use
.Name()for routes you need to generate URLs for - Group Related Routes: Use
Prefix()to organize routes logically - Apply Middleware Appropriately: Use global middleware for cross-cutting concerns, route middleware for specific needs
- Handle Panics Gracefully: The router handles panics automatically, but avoid them when possible
- Test Thoroughly: Write tests for all routes, especially error cases
Performance Tips
- Reuse Router Instance: Create the router once and reuse it
- Use Locals Efficiently: The locals storage uses a slice internally for fast access
- Minimize Middleware: Only apply middleware where needed
- Bind JSON Once: Don’t parse the request body multiple times
- Return Early: Return errors as soon as validation fails
- Use Appropriate Status Codes: Helps with caching and client behavior
