Basic Routing
Steel provides a fast and flexible routing system built on a radix tree for optimal performance. Learn the fundamentals of URL routing, parameter extraction, and route organization.
Creating a Router
import router "github.com/xraph/steel"
func main() {
r := router.NewRouter()
// Your routes here...
log.Fatal(http.ListenAndServe(":8080", r))
}HTTP Methods
Steel supports all standard HTTP methods:
r := router.NewRouter()
r.GET("/users", getUsersHandler)
r.POST("/users", createUserHandler)
r.PUT("/users/:id", updateUserHandler)
r.DELETE("/users/:id", deleteUserHandler)
r.PATCH("/users/:id", patchUserHandler)
r.HEAD("/users/:id", headUserHandler)
r.OPTIONS("/users", optionsHandler)Generic Handler Registration
You can also use the generic Handle method:
r.Handle("GET", "/users", getUsersHandler)
r.Handle("POST", "/users", createUserHandler)
// Or with http.HandlerFunc
r.HandleFunc("GET", "/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
})Path Parameters
Extract dynamic values from URLs using path parameters:
Basic Parameters
// Single parameter
r.GET("/users/:id", func(w http.ResponseWriter, r *http.Request) {
userID := router.URLParam(r, "id")
fmt.Fprintf(w, "User ID: %s", userID)
})
// Multiple parameters
r.GET("/users/:userId/posts/:postId", func(w http.ResponseWriter, r *http.Request) {
userID := router.URLParam(r, "userId")
postID := router.URLParam(r, "postId")
fmt.Fprintf(w, "User: %s, Post: %s", userID, postID)
})URL Examples
// Route: /users/:id
"/users/123" // id = "123"
"/users/abc" // id = "abc"
"/users/user-456" // id = "user-456"
// Route: /users/:userId/posts/:postId
"/users/123/posts/456" // userId = "123", postId = "456"
"/users/john/posts/my-first-post" // userId = "john", postId = "my-first-post"Parameter Constraints
While Steel doesn’t have built-in parameter constraints, you can validate them in your handlers:
r.GET("/users/:id", func(w http.ResponseWriter, r *http.Request) {
idStr := router.URLParam(r, "id")
// Validate numeric ID
id, err := strconv.Atoi(idStr)
if err != nil {
http.Error(w, "Invalid user ID", http.StatusBadRequest)
return
}
if id <= 0 {
http.Error(w, "User ID must be positive", http.StatusBadRequest)
return
}
// Continue with valid ID...
fmt.Fprintf(w, "Valid user ID: %d", id)
})Wildcard Routes
Use wildcards to match any path under a prefix:
// Catch-all wildcard
r.GET("/static/*", func(w http.ResponseWriter, r *http.Request) {
// Serves any file under /static/
filePath := r.URL.Path[len("/static/"):]
fmt.Fprintf(w, "Serving file: %s", filePath)
})
// File server example
r.GET("/assets/*", func(w http.ResponseWriter, r *http.Request) {
fs := http.FileServer(http.Dir("./assets/"))
http.StripPrefix("/assets/", fs).ServeHTTP(w, r)
})Wildcard Examples
// Route: /static/*
"/static/css/style.css" // Matches
"/static/js/app.js" // Matches
"/static/images/logo.png" // Matches
"/static/fonts/arial.woff" // Matches
// Route: /api/v1/*
"/api/v1/users" // Matches
"/api/v1/users/123" // Matches
"/api/v1/posts/456/comments" // MatchesRoute Priority
Steel matches routes in this order:
- Static routes (exact matches)
- Parameter routes (
:param) - Wildcard routes (
*)
r.GET("/users/admin", staticHandler) // Highest priority
r.GET("/users/:id", paramHandler) // Medium priority
r.GET("/users/*", wildcardHandler) // Lowest priority
// URL: /users/admin -> staticHandler
// URL: /users/123 -> paramHandler
// URL: /users/admin/settings -> wildcardHandlerSpecific vs Generic Routes
When defining routes, place more specific routes before generic ones:
// âś… Good: Specific routes first
r.GET("/api/health", healthHandler)
r.GET("/api/version", versionHandler)
r.GET("/api/:resource", genericHandler)
// ❌ Avoid: Generic route would catch everything
r.GET("/api/:resource", genericHandler)
r.GET("/api/health", healthHandler) // Never reached!Route Groups
Organize related routes using route groups:
Basic Grouping
r.Route("/api", func(api router.Router) {
api.GET("/users", getUsersHandler)
api.POST("/users", createUserHandler)
api.GET("/posts", getPostsHandler)
})
// Creates routes:
// GET /api/users
// POST /api/users
// GET /api/postsNested Groups
r.Route("/api", func(api router.Router) {
api.Route("/v1", func(v1 router.Router) {
v1.Route("/users", func(users router.Router) {
users.GET("/", listUsersHandler)
users.POST("/", createUserHandler)
users.GET("/:id", getUserHandler)
users.PUT("/:id", updateUserHandler)
users.DELETE("/:id", deleteUserHandler)
})
})
})
// Creates routes:
// GET /api/v1/users/
// POST /api/v1/users/
// GET /api/v1/users/:id
// PUT /api/v1/users/:id
// DELETE /api/v1/users/:idPractical Example
func setupRoutes() *router.SteelRouter {
r := router.NewRouter()
// Public routes
r.GET("/", homeHandler)
r.GET("/health", healthHandler)
// API v1
r.Route("/api/v1", func(v1 router.Router) {
// Public API endpoints
v1.GET("/products", getProductsHandler)
v1.GET("/products/:id", getProductHandler)
// User management
v1.Route("/users", func(users router.Router) {
users.POST("/register", registerHandler)
users.POST("/login", loginHandler)
// Protected user routes would go here with middleware
})
// Admin endpoints
v1.Route("/admin", func(admin router.Router) {
// Admin routes would include auth middleware
admin.GET("/stats", getStatsHandler)
admin.GET("/users", getAllUsersHandler)
})
})
return r
}Mounting Sub-Routers
Mount existing HTTP handlers or sub-applications:
Basic Mounting
// Mount a file server
fileServer := http.FileServer(http.Dir("./uploads/"))
r.Mount("/uploads", fileServer)
// Mount another HTTP handler
adminMux := http.NewServeMux()
adminMux.HandleFunc("/dashboard", dashboardHandler)
adminMux.HandleFunc("/settings", settingsHandler)
r.Mount("/admin", adminMux)Mounting with Middleware
// Create a sub-router with its own middleware
apiV2 := router.NewRouter()
apiV2.Use(v2AuthMiddleware)
apiV2.Use(v2RateLimitMiddleware)
apiV2.GET("/users", v2GetUsersHandler)
apiV2.POST("/users", v2CreateUserHandler)
// Mount the sub-router
r.Mount("/api/v2", apiV2)Real-world Example
func main() {
r := router.NewRouter()
// Mount Prometheus metrics
r.Mount("/metrics", promhttp.Handler())
// Mount pprof for debugging (development only)
if os.Getenv("ENV") == "development" {
r.Mount("/debug", http.DefaultServeMux)
}
// Mount static file servers
r.Mount("/static", http.FileServer(http.Dir("./static/")))
r.Mount("/uploads", http.FileServer(http.Dir("./uploads/")))
// Mount API documentation
docsHandler := http.FileServer(http.Dir("./docs/"))
r.Mount("/docs", http.StripPrefix("/docs", docsHandler))
// Your API routes
r.Route("/api", func(api router.Router) {
// API routes here...
})
log.Fatal(http.ListenAndServe(":8080", r))
}Query Parameters
Access query parameters using the standard library or Context:
Standard Way
r.GET("/search", func(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query().Get("q")
page := r.URL.Query().Get("page")
limit := r.URL.Query().Get("limit")
fmt.Fprintf(w, "Query: %s, Page: %s, Limit: %s", query, page, limit)
})With Context
r.GET("/search", func(w http.ResponseWriter, r *http.Request) {
ctx := &router.Context{
Request: r,
Response: w,
}
query := ctx.Query("q")
page := ctx.Query("page")
limit := ctx.Query("limit")
fmt.Fprintf(w, "Query: %s, Page: %s, Limit: %s", query, page, limit)
})Query Parameter Examples
// URL: /search?q=golang&page=2&limit=10&sort=date&order=desc
r.URL.Query().Get("q") // "golang"
r.URL.Query().Get("page") // "2"
r.URL.Query().Get("limit") // "10"
r.URL.Query().Get("sort") // "date"
r.URL.Query().Get("order") // "desc"
r.URL.Query().Get("missing") // "" (empty string)
// Multiple values
// URL: /filter?tags=go&tags=web&tags=api
r.URL.Query()["tags"] // []string{"go", "web", "api"}Headers
Access request headers:
r.GET("/api/data", func(w http.ResponseWriter, r *http.Request) {
// Get specific headers
contentType := r.Header.Get("Content-Type")
userAgent := r.Header.Get("User-Agent")
authorization := r.Header.Get("Authorization")
// Custom headers
requestID := r.Header.Get("X-Request-ID")
apiKey := r.Header.Get("X-API-Key")
// Check if header exists
if auth := r.Header.Get("Authorization"); auth == "" {
http.Error(w, "Authorization header required", http.StatusUnauthorized)
return
}
// Set response headers
w.Header().Set("Content-Type", "application/json")
w.Header().Set("X-Response-ID", generateResponseID())
// Response...
})Request Body
Handle request bodies for POST, PUT, and PATCH requests:
JSON Body
type CreateUserRequest struct {
Name string `json:"name"`
Email string `json:"email"`
Age int `json:"age"`
}
r.POST("/users", func(w http.ResponseWriter, r *http.Request) {
var req CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
// Validate request
if req.Name == "" {
http.Error(w, "Name is required", http.StatusBadRequest)
return
}
// Process request...
user := createUser(req.Name, req.Email, req.Age)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
})Form Data
r.POST("/contact", func(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, "Invalid form data", http.StatusBadRequest)
return
}
name := r.FormValue("name")
email := r.FormValue("email")
message := r.FormValue("message")
// Process form...
processContactForm(name, email, message)
w.WriteHeader(http.StatusOK)
w.Write([]byte("Message sent"))
})Raw Body
r.POST("/webhook", func(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read body", http.StatusBadRequest)
return
}
// Verify webhook signature
signature := r.Header.Get("X-Signature")
if !verifySignature(body, signature) {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
// Process webhook...
processWebhook(body)
w.WriteHeader(http.StatusOK)
})Router Configuration
Customize router behavior:
r := router.NewRouter()
// Configure trailing slash behavior
r.SetTrailingSlashRedirect(true) // Redirect /users/ to /users
r.SetFixedPathRedirect(true) // Handle case-insensitive redirects
// Custom 404 handler
r.SetNotFoundHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNotFound)
json.NewEncoder(w).Encode(map[string]string{
"error": "Not Found",
"message": "The requested resource was not found",
"path": r.URL.Path,
})
}))
// Custom method not allowed handler
r.SetMethodNotAllowedHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusMethodNotAllowed)
json.NewEncoder(w).Encode(map[string]string{
"error": "Method Not Allowed",
"message": fmt.Sprintf("Method %s is not allowed for this resource", r.Method),
"path": r.URL.Path,
})
}))Route Debugging
Debug your routes during development:
r := router.NewRouter()
// Add some routes
r.GET("/", homeHandler)
r.GET("/users/:id", getUserHandler)
r.POST("/users", createUserHandler)
r.GET("/static/*", staticHandler)
// Print route tree (development only)
if os.Getenv("ENV") == "development" {
r.DebugRoutes()
}Output example:
=== Steel Debug Info ===
Method: GET
Path: "", HasHandler: true, IsParam: false, ParamName: "", Wildcard: false
Path: "users", HasHandler: false, IsParam: false, ParamName: "", Wildcard: false
Path: "", HasHandler: false, IsParam: true, ParamName: "id", Wildcard: false
Path: "static", HasHandler: false, IsParam: false, ParamName: "", Wildcard: false
Path: "", HasHandler: true, IsParam: false, ParamName: "", Wildcard: true
Method: POST
Path: "users", HasHandler: true, IsParam: false, ParamName: "", Wildcard: falsePerformance Considerations
Performance: Steel uses a radix tree for O(log n) route lookup and pools parameter objects to minimize allocations.
Route Organization
// âś… Good: Organize routes logically
r.Route("/api/v1", func(v1 router.Router) {
v1.Route("/users", func(users router.Router) {
users.GET("/", listUsers)
users.POST("/", createUser)
users.GET("/:id", getUser)
users.PUT("/:id", updateUser)
users.DELETE("/:id", deleteUser)
})
})
// âś… Good: Group related functionality
r.Route("/admin", func(admin router.Router) {
admin.Use(adminAuthMiddleware)
// All admin routes here
})Efficient Parameter Extraction
// âś… Efficient: Extract parameters once
r.GET("/users/:userId/posts/:postId", func(w http.ResponseWriter, r *http.Request) {
params := router.ParamsFromContext(r.Context())
userID := params.Get("userId")
postID := params.Get("postId")
// Use userID and postID...
})
// ❌ Less efficient: Multiple extractions
r.GET("/users/:userId/posts/:postId", func(w http.ResponseWriter, r *http.Request) {
userID := router.URLParam(r, "userId") // Extracts from context
postID := router.URLParam(r, "postId") // Extracts from context again
// This works but is less efficient
})Common Patterns
RESTful Resource Routes
func setupUserRoutes(r router.Router) {
r.Route("/users", func(users router.Router) {
users.GET("/", listUsers) // GET /users
users.POST("/", createUser) // POST /users
users.GET("/:id", getUser) // GET /users/:id
users.PUT("/:id", updateUser) // PUT /users/:id
users.PATCH("/:id", patchUser) // PATCH /users/:id
users.DELETE("/:id", deleteUser) // DELETE /users/:id
// Nested resources
users.Route("/:id/posts", func(posts router.Router) {
posts.GET("/", getUserPosts) // GET /users/:id/posts
posts.POST("/", createUserPost) // POST /users/:id/posts
})
})
}API Versioning
func setupVersionedAPI(r *router.SteelRouter) {
// Version 1
r.Route("/api/v1", func(v1 router.Router) {
v1.GET("/users", v1GetUsers)
v1.POST("/users", v1CreateUser)
})
// Version 2 with breaking changes
r.Route("/api/v2", func(v2 router.Router) {
v2.Use(v2Middleware)
v2.GET("/users", v2GetUsers)
v2.POST("/users", v2CreateUser)
})
// Latest version alias
r.Route("/api/latest", func(latest router.Router) {
latest.GET("/users", v2GetUsers)
latest.POST("/users", v2CreateUser)
})
}Content Negotiation
r.GET("/api/data", func(w http.ResponseWriter, r *http.Request) {
data := getData()
switch r.Header.Get("Accept") {
case "application/xml":
w.Header().Set("Content-Type", "application/xml")
xml.NewEncoder(w).Encode(data)
case "text/csv":
w.Header().Set("Content-Type", "text/csv")
writeCSV(w, data)
default: // JSON
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(data)
}
})Basic routing in Steel provides a solid foundation for building scalable web applications with clean, maintainable URL structures.