Routing
Azu's routing system provides a powerful, type-safe way to define and handle HTTP requests. Built on the high-performance Radix tree, it offers both compile-time route registration and runtime pattern matching.
Overview
Basic Routing
HTTP Method Registration
struct UserEndpoint
include Endpoint(UserRequest, UserResponse)
# Standard HTTP methods
get "/users"
post "/users"
put "/users/:id"
patch "/users/:id"
delete "/users/:id"
# Custom methods
head "/users"
options "/users"
def call : UserResponse
case request.method
when .get?
list_users
when .post?
create_user
when .put?, .patch?
update_user
when .delete?
delete_user
end
end
end
Route Parameters
struct BlogEndpoint
include Endpoint(BlogRequest, BlogResponse)
# Simple parameter
get "/posts/:id"
# Multiple parameters
get "/posts/:id/comments/:comment_id"
# Optional parameters (using query strings)
get "/posts"
def call : BlogResponse
case request.path
when /^\/posts\/(\d+)$/
show_post($1.to_i)
when /^\/posts\/(\d+)\/comments\/(\d+)$/
show_comment($1.to_i, $2.to_i)
when /^\/posts$/
list_posts
end
end
end
Advanced Routing Patterns
Nested Routes
struct ApiEndpoint
include Endpoint(ApiRequest, ApiResponse)
# Nested resource routes
get "/api/v1/users/:user_id/posts/:post_id"
post "/api/v1/users/:user_id/posts"
get "/api/v1/users/:user_id/posts"
# Namespace routes
get "/api/v1/admin/users"
post "/api/v1/admin/users"
def call : ApiResponse
case request.path
when /^\/api\/v1\/users\/(\d+)\/posts\/(\d+)$/
show_user_post($1.to_i, $2.to_i)
when /^\/api\/v1\/users\/(\d+)\/posts$/
if request.post?
create_user_post($1.to_i)
else
list_user_posts($1.to_i)
end
when /^\/api\/v1\/admin\/users$/
if request.post?
create_admin_user
else
list_admin_users
end
end
end
end
Wildcard Routes
struct StaticEndpoint
include Endpoint(StaticRequest, StaticResponse)
# Catch-all route for static files
get "/static/*"
# Specific file types
get "/images/*.jpg"
get "/images/*.png"
get "/documents/*.pdf"
def call : StaticResponse
case request.path
when /^\/static\/(.+)$/
serve_static_file($1)
when /^\/images\/(.+)\.(jpg|png)$/
serve_image($1, $2)
when /^\/documents\/(.+)\.pdf$/
serve_document($1)
end
end
end
Route Constraints
Type Constraints
struct ConstrainedEndpoint
include Endpoint(ConstrainedRequest, ConstrainedResponse)
# Numeric constraints
get "/users/:id", constraints: {id: /\d+/}
get "/posts/:year/:month", constraints: {
year: /\d{4}/,
month: /\d{2}/
}
# Custom constraints
get "/categories/:slug", constraints: {slug: /[a-z0-9-]+/}
def call : ConstrainedResponse
case request.path
when /^\/users\/(\d+)$/
show_user($1.to_i)
when /^\/posts\/(\d{4})\/(\d{2})$/
show_posts_by_month($1.to_i, $2.to_i)
when /^\/categories\/([a-z0-9-]+)$/
show_category($1)
end
end
end
HTTP Method Constraints
struct MethodConstrainedEndpoint
include Endpoint(MethodRequest, MethodResponse)
# Method-specific routes
get "/api/users", only: [:index, :show]
post "/api/users", only: [:create]
put "/api/users/:id", only: [:update]
delete "/api/users/:id", only: [:destroy]
def call : MethodResponse
case {request.method, request.path}
when {.get?, /^\/api\/users$/}
list_users
when {.get?, /^\/api\/users\/(\d+)$/}
show_user($1.to_i)
when {.post?, /^\/api\/users$/}
create_user
when {.put?, /^\/api\/users\/(\d+)$/}
update_user($1.to_i)
when {.delete?, /^\/api\/users\/(\d+)$/}
delete_user($1.to_i)
end
end
end
Route Groups and Namespaces
Grouped Routes
struct GroupedEndpoint
include Endpoint(GroupedRequest, GroupedResponse)
# Route groups with common prefix
group "/api/v1" do
get "/users"
post "/users"
get "/users/:id"
put "/users/:id"
delete "/users/:id"
end
# Nested groups
group "/api/v1" do
group "/admin" do
get "/users"
post "/users"
end
group "/public" do
get "/posts"
get "/posts/:id"
end
end
def call : GroupedResponse
case request.path
when /^\/api\/v1\/users$/
handle_users
when /^\/api\/v1\/users\/(\d+)$/
handle_user($1.to_i)
when /^\/api\/v1\/admin\/users$/
handle_admin_users
when /^\/api\/v1\/public\/posts$/
handle_public_posts
when /^\/api\/v1\/public\/posts\/(\d+)$/
handle_public_post($1.to_i)
end
end
end
Namespaced Routes
struct NamespacedEndpoint
include Endpoint(NamespacedRequest, NamespacedResponse)
# Namespace with module organization
namespace "/api/v1" do
namespace "/users" do
get "/"
post "/"
get "/:id"
put "/:id"
delete "/:id"
end
namespace "/posts" do
get "/"
post "/"
get "/:id"
put "/:id"
delete "/:id"
end
end
def call : NamespacedResponse
case request.path
when /^\/api\/v1\/users\/$/
list_users
when /^\/api\/v1\/users\/(\d+)$/
handle_user($1.to_i)
when /^\/api\/v1\/posts\/$/
list_posts
when /^\/api\/v1\/posts\/(\d+)$/
handle_post($1.to_i)
end
end
end
Route Helpers
Named Routes
struct NamedRouteEndpoint
include Endpoint(NamedRequest, NamedResponse)
# Named routes for easy reference
get "/users/:id", as: :user_path
get "/posts/:id", as: :post_path
get "/categories/:slug", as: :category_path
def call : NamedResponse
# Generate URLs using route helpers
user_url = user_path(id: 123)
post_url = post_path(id: 456)
category_url = category_path(slug: "technology")
# Use in responses
NamedResponse.new(
links: {
user: user_url,
post: post_url,
category: category_url
}
)
end
end
URL Generation
# Route helper methods
def user_path(id : Int32) : String
"/users/#{id}"
end
def post_path(id : Int32) : String
"/posts/#{id}"
end
def category_path(slug : String) : String
"/categories/#{slug}"
end
# With query parameters
def users_path(page : Int32? = nil, per_page : Int32? = nil) : String
path = "/users"
params = [] of String
params << "page=#{page}" if page
params << "per_page=#{per_page}" if per_page
path += "?#{params.join("&")}" unless params.empty?
path
end
Route Middleware
Route-Specific Middleware
struct MiddlewareEndpoint
include Endpoint(MiddlewareRequest, MiddlewareResponse)
# Apply middleware to specific routes
get "/admin/users", middleware: [AuthMiddleware, AdminMiddleware]
get "/api/users", middleware: [ApiAuthMiddleware]
get "/public/posts", middleware: [CacheMiddleware]
def call : MiddlewareResponse
case request.path
when /^\/admin\/users$/
list_admin_users
when /^\/api\/users$/
list_api_users
when /^\/public\/posts$/
list_public_posts
end
end
end
Conditional Middleware
struct ConditionalMiddlewareEndpoint
include Endpoint(ConditionalRequest, ConditionalResponse)
# Conditional middleware based on environment
get "/api/users", middleware: conditional_middleware
private def conditional_middleware
if Azu::Environment.production?
[RateLimitMiddleware, AuthMiddleware]
else
[AuthMiddleware]
end
end
def call : ConditionalResponse
list_users
end
end
Route Testing
Unit Testing Routes
describe "UserEndpoint" do
it "routes GET /users to list_users" do
endpoint = UserEndpoint.new
request = create_request("GET", "/users")
response = endpoint.call(request)
assert response.status == 200
assert response.body.includes?("users")
end
it "routes GET /users/:id to show_user" do
endpoint = UserEndpoint.new
request = create_request("GET", "/users/123")
response = endpoint.call(request)
assert response.status == 200
assert response.body.includes?("user 123")
end
end
Integration Testing
describe "Routing Integration" do
it "handles nested routes correctly" do
app = TestApp.new
response = app.get("/api/v1/users/123/posts/456")
assert response.status == 200
assert response.body.includes?("user 123 post 456")
end
it "applies middleware correctly" do
app = TestApp.new
response = app.get("/admin/users")
# Should be redirected if not authenticated
assert response.status == 302
end
end
Performance Considerations
Route Tree Optimization
# Efficient route matching
struct OptimizedEndpoint
include Endpoint(OptimizedRequest, OptimizedResponse)
# Group similar routes together
get "/api/v1/users"
get "/api/v1/users/:id"
get "/api/v1/users/:id/posts"
# Use specific constraints to reduce matching time
get "/api/v1/posts/:year/:month", constraints: {
year: /\d{4}/,
month: /\d{2}/
}
def call : OptimizedResponse
# Fast path matching
case request.path
when /^\/api\/v1\/users$/
list_users
when /^\/api\/v1\/users\/(\d+)$/
show_user($1.to_i)
when /^\/api\/v1\/users\/(\d+)\/posts$/
show_user_posts($1.to_i)
when /^\/api\/v1\/posts\/(\d{4})\/(\d{2})$/
show_posts_by_month($1.to_i, $2.to_i)
end
end
end
Caching Routes
# Cache compiled routes for better performance
class RouteCache
@@compiled_routes = {} of String => Regex
def self.get_route(pattern : String) : Regex
@@compiled_routes[pattern] ||= Regex.new(pattern)
end
end
Best Practices
1. Use Descriptive Route Names
# Good: Clear and descriptive
get "/api/v1/users/:id/posts/:post_id/comments"
get "/api/v1/admin/users/:id/permissions"
# Avoid: Unclear or ambiguous
get "/api/v1/u/:id/p/:pid/c"
get "/api/v1/a/u/:id/p"
2. Group Related Routes
# Good: Logical grouping
group "/api/v1" do
group "/users" do
get "/"
post "/"
get "/:id"
put "/:id"
delete "/:id"
end
end
# Avoid: Scattered routes
get "/api/v1/users"
post "/api/v1/users"
get "/api/v1/users/:id"
put "/api/v1/users/:id"
delete "/api/v1/users/:id"
3. Use Constraints for Validation
# Good: Validate parameters at routing level
get "/users/:id", constraints: {id: /\d+/}
get "/posts/:year/:month", constraints: {
year: /\d{4}/,
month: /\d{2}/
}
# Avoid: No validation
get "/users/:id"
get "/posts/:year/:month"
4. Keep Routes RESTful
# Good: RESTful conventions
get "/users" # List users
post "/users" # Create user
get "/users/:id" # Show user
put "/users/:id" # Update user
delete "/users/:id" # Delete user
# Avoid: Non-RESTful patterns
get "/get_users"
post "/create_user"
get "/show_user/:id"
post "/update_user/:id"
post "/delete_user/:id"
Next Steps
Endpoints - Learn about endpoint implementation
Request Contracts - Understand request handling
Response Objects - Explore response patterns
Middleware - Add middleware to routes
Last updated
Was this helpful?