Routing
Define routes in Velocity with automatic discovery, RESTful resources, route groups, and gorilla/mux-based routing.
Velocity provides a powerful and expressive routing system built on top of the battle-tested gorilla/mux router. It offers automatic route discovery, RESTful resources, Context-based handlers, and a clean API inspired by frameworks like Fiber and Echo.
Quick Start
Define routes in your application’s routes/ directory. Each file uses init() functions to automatically register routes:
// routes/web.go
package routes
import (
"myapp/internal/handlers"
"github.com/velocitykode/velocity/pkg/router"
)
func init() {
router.Register(func(r router.Router) {
// Create handler instances
homeHandler := handlers.NewHomeHandler()
aboutHandler := handlers.NewAboutHandler()
r.Get("/", homeHandler.Index)
r.Get("/about", aboutHandler.Index)
})
}Automatic Route Discovery
Velocity automatically discovers and loads all route files in your routes/ directory. Simply import the routes package in your main.go:
package main
import (
_ "myapp/routes" // Auto-register all routes
"github.com/velocitykode/velocity/pkg/router"
)
func main() {
// Load all registered routes
router.LoadRoutes()
// Start server
http.ListenAndServe(":4000", router.Get())
}Context-Based Handlers
All route handlers use router.HandlerFunc which receives a *router.Context and returns an error:
func (c *UserHandler) Index(ctx *router.Context) error {
users, err := models.User{}.All()
if err != nil {
return err
}
view.Render(ctx.Response, ctx.Request, "users/index", view.Props{
"users": users,
})
return nil
}The Context provides convenient methods for accessing request data:
func (c *UserHandler) Show(ctx *router.Context) error {
// Get URL parameter
id := ctx.Param("id")
// Get query parameter
filter := ctx.Query("filter")
// Bind JSON body
var input CreateUserInput
if err := ctx.Bind(&input); err != nil {
return err
}
// Return JSON response
return ctx.JSON(200, user)
}HTTP Methods
The router supports all standard HTTP methods:
r.Get("/users", handler)
r.Post("/users", handler)
r.Put("/users/{id}", handler)
r.Delete("/users/{id}", handler)
r.Patch("/users/{id}", handler)
r.Options("/users", handler)
r.Head("/users", handler)Route Parameters
Basic Parameters
Capture URL segments using curly braces:
r.Get("/users/{id}", func(ctx *router.Context) error {
id := ctx.Param("id")
// Use the id parameter
return nil
})Multiple Parameters
r.Get("/posts/{post}/comments/{comment}", func(ctx *router.Context) error {
postID := ctx.Param("post")
commentID := ctx.Param("comment")
return nil
})Parameter Constraints
Use regex to constrain parameter values:
// Only match numeric IDs
r.Get("/users/{id:[0-9]+}", handler)
// Match slugs
r.Get("/posts/{slug:[a-z0-9-]+}", handler)
// UUID format
r.Get("/api/{uuid:[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}}", handler)RESTful Resources
Following RESTful conventions with standard handler methods:
// routes/user.go
func init() {
router.Register(func(r router.Router) {
// Create handler instance
userHandler := handlers.UserHandler{}
// RESTful routes
r.Get("/users", userHandler.Index) // List all
r.Get("/users/create", userHandler.Create) // Show form
r.Post("/users", userHandler.Store) // Save new
r.Get("/users/{id}", userHandler.Show) // Display one
r.Get("/users/{id}/edit", userHandler.Edit) // Edit form
r.Put("/users/{id}", userHandler.Update) // Update
r.Delete("/users/{id}", userHandler.Destroy) // Delete
})
}RESTful Conventions
Common RESTful route patterns:
GET /users- List all resourcesGET /users/create- Show create formPOST /users- Store new resourceGET /users/{id}- Show specific resourceGET /users/{id}/edit- Show edit formPUT /users/{id}- Update resourceDELETE /users/{id}- Delete resource
Route Groups
Group related routes to share common paths:
func init() {
router.Register(func(r router.Router) {
// Create handler instances
dashboardHandler := handlers.Admin.DashboardHandler{}
adminUserHandler := handlers.Admin.UserHandler{}
settingsHandler := handlers.Admin.SettingsHandler{}
apiUserHandler := handlers.API.UserHandler{}
apiPostHandler := handlers.API.PostHandler{}
// Admin routes group
admin := r.Group("/admin")
{
admin.Get("/", dashboardHandler.Index)
admin.Get("/users", adminUserHandler.Index)
admin.Get("/settings", settingsHandler.Index)
}
// API routes with versioning
api := r.Group("/api")
{
v1 := api.Group("/v1")
{
v1.Get("/users", apiUserHandler.Index)
v1.Get("/posts", apiPostHandler.Index)
}
}
})
}Route Prefixes
Apply a prefix to all routes in a file:
// routes/api.go
func init() {
router.Register(func(r router.Router) {
r.Prefix("/api/v1")
r.Get("/users", handler) // GET /api/v1/users
r.Get("/posts", handler) // GET /api/v1/posts
})
}Named Routes
Name routes for easy URL generation:
// Define named route
r.Get("/users/{id}", handler).Name("users.show")
// Generate URL
url, _ := router.Route("users.show", map[string]string{"id": "123"})
// Returns: /users/123Route Organization
Best Practices
One file per resource: Create separate files for each resource
routes/ ├── web.go # General web routes ├── user.go # User routes ├── post.go # Post routes └── auth.go # Authentication routesUse init() for registration: Each file registers its own routes
func init() { router.Register(func(r router.Router) { // Route definitions }) }Group related routes: Use groups for admin, API, etc.
Follow RESTful conventions: Use standard method names
Testing Routes
Test your routes and handlers:
func TestUserRoutes(t *testing.T) {
// Load routes
router.LoadRoutes()
// Create test request
req := httptest.NewRequest("GET", "/users", nil)
w := httptest.NewRecorder()
// Execute request
router.Get().ServeHTTP(w, req)
// Assert response
if w.Code != http.StatusOK {
t.Errorf("Expected status 200, got %d", w.Code)
}
}Handler Example
Here’s a complete handler example using the Context-based handlers:
package handlers
import (
"github.com/velocitykode/velocity/pkg/router"
"github.com/velocitykode/velocity/pkg/view"
)
type UserHandler struct{}
func NewUserHandler() *UserHandler {
return &UserHandler{}
}
func (c *UserHandler) Index(ctx *router.Context) error {
users, _ := models.User{}.All()
view.Render(ctx.Response, ctx.Request, "users/index", view.Props{
"users": users,
})
return nil
}
func (c *UserHandler) Show(ctx *router.Context) error {
id := ctx.Param("id")
user, err := models.User{}.Find(id)
if err != nil {
return ctx.JSON(404, map[string]string{"error": "User not found"})
}
view.Render(ctx.Response, ctx.Request, "users/show", view.Props{
"user": user,
})
return nil
}
func (c *UserHandler) Store(ctx *router.Context) error {
var input struct {
Name string `json:"name"`
Email string `json:"email"`
}
if err := ctx.Bind(&input); err != nil {
return ctx.JSON(400, map[string]string{"error": "Invalid input"})
}
user, err := models.User{}.Create(map[string]any{
"name": input.Name,
"email": input.Email,
})
if err != nil {
return err
}
return ctx.JSON(201, user)
}Using Handlers in Routes
Store handler instances in variables for cleaner code:
func init() {
router.Register(func(r router.Router) {
// Create handler instances
userHandler := handlers.NewUserHandler()
postHandler := handlers.NewPostHandler()
// User routes
r.Get("/users", userHandler.Index)
r.Get("/users/{id}", userHandler.Show)
r.Post("/users", userHandler.Store)
// Post routes
r.Get("/posts", postHandler.Index)
r.Get("/posts/{id}", postHandler.Show)
})
}Configuration
Configure routing behavior in your .env file:
# Server port
PORT=4000
# Enable route debugging
ROUTE_DEBUG=false
# Enable route caching (production)
ROUTE_CACHE=true
