Routing Guide
Varel's routing system combines the performance of radix tree lookups with the elegance of Roda-style path consumption. This guide covers everything you need to know about routing in Varel.
Table of Contents
- Basic Routing
- HTTP Methods
- Route Parameters
- Query Strings
- Route Groups
- Middleware on Routes
- Route Priority
- Wildcard Routes
- Route Patterns
- Performance
Basic Routing
Routes map HTTP requests to handler functions.
File: routes.v - All route definitions go in this file!
module main
import leafscale.varel.app as varel_app
import leafscale.varel.http
import controllers
fn register_routes(mut app &varel_app.App) ! {
mut home_ctrl := controllers.HomeController{}
// Simple routes
app.get('/', fn [mut home_ctrl] (mut ctx http.Context) http.Response {
return home_ctrl.index(mut ctx)
})!
app.get('/about', fn [mut home_ctrl] (mut ctx http.Context) http.Response {
return home_ctrl.about(mut ctx)
})!
app.get('/contact', fn [mut home_ctrl] (mut ctx http.Context) http.Response {
return home_ctrl.contact(mut ctx)
})!
}
Note: For brevity, subsequent examples in this guide show simplified code. In a real Varel application:
- Routes go in
routes.vinside theregister_routes()function - Controllers go in
controllers/directory (one file per resource) - The examples below show the routing code only - adapt them for your
routes.vfile
HTTP Methods
Varel supports all standard HTTP methods:
fn main() {
mut web_app := varel_app.new('My App')
// GET - Retrieve resource
web_app.get('/users', list_users)!
// POST - Create resource
web_app.post('/users', create_user)!
// PUT - Update entire resource
web_app.put('/users/:id', update_user)!
// PATCH - Partially update resource
web_app.patch('/users/:id', patch_user)!
// DELETE - Delete resource
web_app.delete('/users/:id', delete_user)!
// HEAD - Get headers only (no body)
web_app.head('/users', users_head)!
// OPTIONS - Get allowed methods
web_app.options('/users', users_options)!
web_app.listen(':8080')
}
RESTful Resource Routes
For RESTful APIs, Varel follows these conventions:
| HTTP Method | Path | Action | Purpose |
|---|---|---|---|
| GET | /users |
index | List all users |
| GET | /users/:id |
show | Show single user |
| GET | /users/new |
new | Show create form |
| POST | /users |
create | Create new user |
| GET | /users/:id/edit |
edit | Show edit form |
| PUT/PATCH | /users/:id |
update | Update user |
| DELETE | /users/:id |
destroy | Delete user |
fn main() {
mut web_app := varel_app.new('My App')
// List all users
web_app.get('/users', users_index)!
// Show create form
web_app.get('/users/new', users_new)!
// Create new user
web_app.post('/users', users_create)!
// Show single user
web_app.get('/users/:id', users_show)!
// Show edit form
web_app.get('/users/:id/edit', users_edit)!
// Update user
web_app.put('/users/:id', users_update)!
web_app.patch('/users/:id', users_update)! // Also allow PATCH
// Delete user
web_app.delete('/users/:id', users_destroy)!
web_app.listen(':8080')
}
Route Parameters
Named Parameters
Extract dynamic values from URLs using :param syntax:
fn main() {
mut web_app := varel_app.new('My App')
// Single parameter
web_app.get('/users/:id', show_user)!
// Multiple parameters
web_app.get('/users/:user_id/posts/:post_id', show_user_post)!
// Mixed static and dynamic segments
web_app.get('/api/v1/products/:id', show_product)!
web_app.listen(':8080')
}
fn show_user(mut ctx http.Context) http.Response {
// Get parameter as string
id := ctx.param('id')
// Convert to int
user_id := id.int()
return ctx.text(200, 'User ID: ${user_id}')
}
fn show_user_post(mut ctx http.Context) http.Response {
user_id := ctx.param('user_id').int()
post_id := ctx.param('post_id').int()
return ctx.text(200, 'User ${user_id}, Post ${post_id}')
}
Parameter Type Conversion
Parameters are always strings. Convert them as needed:
fn handler(mut ctx http.Context) http.Response {
// String parameter
name := ctx.param('name')
// Integer parameter
id := ctx.param('id').int()
// Float parameter
price := ctx.param('price').f64()
// Boolean parameter (manual conversion)
enabled_str := ctx.param('enabled')
enabled := enabled_str == 'true' || enabled_str == '1'
// Optional parameter (may not exist)
category := ctx.param('category') or { 'default' }
return ctx.ok('Processed')
}
Parameter Validation
Always validate parameters:
fn show_user(mut ctx http.Context) http.Response {
// Get parameter
id_str := ctx.param('id')
// Validate - is it a number?
id := id_str.int()
if id <= 0 {
return ctx.bad_request('Invalid user ID')
}
// Fetch user
user := db.get_user(id) or {
return ctx.not_found('User not found')
}
return ctx.json_response(200, user)
}
Query Strings
Access query string parameters with ctx.query():
fn search(mut ctx http.Context) http.Response {
// URL: /search?q=varel&page=2&limit=20
// Get query parameters
query := ctx.query('q') or { '' }
page_str := ctx.query('page') or { '1' }
limit_str := ctx.query('limit') or { '10' }
// Convert types
page := page_str.int()
limit := limit_str.int()
// Validate
if query == '' {
return ctx.bad_request('Search query is required')
}
if page < 1 {
return ctx.bad_request('Page must be >= 1')
}
if limit < 1 || limit > 100 {
return ctx.bad_request('Limit must be between 1 and 100')
}
// Perform search
results := perform_search(query, page, limit)
return ctx.json_response(200, {
'query': query
'page': page
'limit': limit
'results': results
})
}
Multiple Query Parameters
Handle arrays and multiple values:
fn filter_products(mut ctx http.Context) http.Response {
// URL: /products?category=electronics&category=books&sort=price
// Get single value
sort := ctx.query('sort') or { 'name' }
// Get all values for a parameter
categories := ctx.query_all('category') // ['electronics', 'books']
// Build filter
products := db.get_products()
.filter_categories(categories)
.sort_by(sort)
return ctx.json_response(200, products)
}
Route Groups
Group related routes with shared prefixes and middleware.
File: routes.v - Add route groups inside register_routes():
fn register_routes(mut app &varel_app.App) ! {
// Public routes
app.get('/', index)!
// Admin routes with shared prefix and middleware
mut admin := app.group('/admin')
admin.use(middleware.auth_required())
admin.use(middleware.audit_log())
admin.get('/dashboard', admin_dashboard)!
admin.get('/users', admin_users)!
admin.post('/users', admin_create_user)!
// API v1 routes
mut api_v1 := app.group('/api/v1')
api_v1.use(middleware.rate_limit_default())
api_v1.use(middleware.api_key_auth())
api_v1.get('/users', api_users)!
api_v1.post('/users', api_create_user)!
// API v2 routes
mut api_v2 := app.group('/api/v2')
api_v2.use(middleware.rate_limit_default())
api_v2.use(middleware.oauth_auth())
api_v2.get('/users', api_v2_users)!
api_v2.post('/users', api_v2_create_user)!
}
Nested Groups
Groups can be nested for hierarchical organization:
fn main() {
mut web_app := varel_app.new('My App')
// API group
api := web_app.group('/api')
api.use(middleware.json_only())
// API v1
v1 := api.group('/v1')
v1.get('/users', api_v1_users)!
// API v1 admin
v1_admin := v1.group('/admin')
v1_admin.use(middleware.admin_only())
v1_admin.get('/stats', api_v1_admin_stats)!
// Results in routes:
// GET /api/v1/users
// GET /api/v1/admin/stats (with admin middleware)
web_app.listen(':8080')
}
Middleware on Routes
Apply middleware at three levels:
1. Global Middleware
Runs for all routes.
File: middleware.v - Configure global middleware:
fn configure_middleware(mut app &varel_app.App) {
// Global middleware (runs for ALL routes)
app.use(middleware.logger_default())
app.use(middleware.recovery_default())
app.use(middleware.cors_default())
}
2. Group Middleware
Runs for all routes in a group.
File: routes.v - Apply middleware to route groups:
fn register_routes(mut app &varel_app.App) ! {
// Public routes (no auth required)
app.get('/', index)!
// Admin routes (auth required for all)
mut admin := app.group('/admin')
admin.use(middleware.auth_required()) // Group middleware
admin.get('/dashboard', dashboard)!
admin.get('/users', users)!
}
3. Route-Specific Middleware
Runs for a single route only.
File: routes.v - Apply middleware to individual routes:
fn register_routes(mut app &varel_app.App) ! {
// Normal routes
app.get('/public', public_handler)!
// Route with specific middleware
app.get('/expensive', expensive_handler)!
.use(middleware.rate_limit_strict()) // Only for this route
.use(middleware.cache(ttl: 3600)) // Only for this route
}
Middleware Order
Middleware executes in this order:
Request
↓
Global Middleware 1
↓
Global Middleware 2
↓
Group Middleware 1
↓
Group Middleware 2
↓
Route Middleware 1
↓
Route Middleware 2
↓
Route Handler
↓
Response (middleware in reverse order)
Example:
fn main() {
mut web_app := varel_app.new('My App')
// 1. Global - runs first
web_app.use(middleware.logger())
// Admin group
admin := web_app.group('/admin')
// 2. Group - runs second
admin.use(middleware.auth_required())
// 3. Route - runs third
admin.get('/users', admin_users)!
.use(middleware.admin_only())
// Execution order for GET /admin/users:
// logger → auth_required → admin_only → handler
}
Route Priority
When multiple routes match, Varel uses priority rules:
Priority Order (Highest to Lowest)
Exact static matches
web_app.get('/users/new', handler)! // Highest priorityStatic prefixes with parameters
web_app.get('/users/:id', handler)! // Medium priorityCatchall/wildcard routes
web_app.get('/users/*path', handler)! // Lowest priority
Example
fn main() {
mut web_app := varel_app.new('My App')
// These routes are checked in priority order:
// 1. Exact match (highest priority)
web_app.get('/users/new', show_new_form)!
// 2. Parameter match
web_app.get('/users/:id', show_user)!
// 3. Catchall (lowest priority)
web_app.get('/users/*path', users_catchall)!
web_app.listen(':8080')
}
// URL matching examples:
// GET /users/new → show_new_form (exact match wins)
// GET /users/123 → show_user (parameter match)
// GET /users/abc/xyz → users_catchall (catchall matches rest)
Order Matters
Define more specific routes before general ones:
// ✅ CORRECT - Specific before general
web_app.get('/api/health', health_check)!
web_app.get('/api/:version', api_version)!
// ❌ WRONG - General before specific
web_app.get('/api/:version', api_version)!
web_app.get('/api/health', health_check)! // Never reached!
Wildcard Routes
Capture multiple path segments with *param:
fn main() {
mut web_app := varel_app.new('My App')
// Catch all paths under /docs/
web_app.get('/docs/*path', serve_docs)!
// Catch all unmatched routes (404 handler)
web_app.get('/*path', not_found)!
web_app.listen(':8080')
}
fn serve_docs(mut ctx http.Context) http.Response {
// Get the captured path
path := ctx.param('path')
// URL: /docs/guide/routing.html
// path = "guide/routing.html"
// Serve file from docs directory
file_path := './docs/${path}'
if !os.exists(file_path) {
return ctx.not_found('Documentation not found')
}
content := os.read_file(file_path) or {
return ctx.internal_error('Failed to read file')
}
return ctx.html(200, content)
}
fn not_found(mut ctx http.Context) http.Response {
path := ctx.param('path')
return ctx.not_found('Page not found: /${path}')
}
Route Patterns
Root Route
web_app.get('/', index)! // Matches: /
Static Routes
web_app.get('/about', about)! // Matches: /about
web_app.get('/contact/form', form)! // Matches: /contact/form
Single Parameter
web_app.get('/users/:id', show)! // Matches: /users/123
// Param: id = "123"
Multiple Parameters
web_app.get('/users/:user_id/posts/:post_id', show)!
// Matches: /users/5/posts/10
// Params: user_id = "5", post_id = "10"
Mixed Static and Dynamic
web_app.get('/api/v1/users/:id', show)!
// Matches: /api/v1/users/123
// Param: id = "123"
Wildcard
web_app.get('/static/*filepath', serve)!
// Matches: /static/css/main.css
// Param: filepath = "css/main.css"
Trailing Slash
Varel normalizes paths, so trailing slashes don't matter:
web_app.get('/about', about)!
// Both match:
// /about
// /about/
Performance
Varel's routing is fast thanks to the radix tree algorithm:
Complexity
- Lookup Time: O(k) where k = path depth
- Memory: O(n) where n = number of routes
- Insertion: O(m) where m = route length
Benchmarks
Routes: 1,000
Avg Path Depth: 4
Lookup Time: ~500 nanoseconds
Routes: 10,000
Avg Path Depth: 4
Lookup Time: ~600 nanoseconds
Conclusion: Lookup time is independent of route count!
Best Practices for Performance
Use Static Paths When Possible
// ✅ Fast - static lookup web_app.get('/api/users', handler)! // ❌ Slower - dynamic lookup web_app.get('/api/:resource', handler)!Limit Wildcards
// ✅ Better - specific routes web_app.get('/docs/guide', guide)! web_app.get('/docs/api', api)! // ❌ Slower - catchall web_app.get('/docs/*path', docs)!Group Related Routes
// ✅ Efficient - shared prefix api := web_app.group('/api/v1') api.get('/users', users)! api.get('/posts', posts)!Keep Path Depth Reasonable
// ✅ Good - depth of 3 web_app.get('/api/v1/users', handler)! // ❌ Deep nesting - depth of 6 web_app.get('/api/v1/admin/internal/system/users', handler)!
Advanced Patterns
Optional Segments
Handle optional URL segments:
fn main() {
mut web_app := varel_app.new('My App')
// List all posts or posts by category
web_app.get('/posts', list_posts)!
web_app.get('/posts/category/:category', list_posts_by_category)!
web_app.listen(':8080')
}
fn list_posts(mut ctx http.Context) http.Response {
// List all posts
posts := db.get_all_posts()
return ctx.json_response(200, posts)
}
fn list_posts_by_category(mut ctx http.Context) http.Response {
category := ctx.param('category')
posts := db.get_posts_by_category(category)
return ctx.json_response(200, posts)
}
Regex-Like Patterns
Use multiple routes for pattern matching:
fn main() {
mut web_app := varel_app.new('My App')
// UUID pattern (manual validation in handler)
web_app.get('/users/:uuid', show_user)!
web_app.listen(':8080')
}
fn show_user(mut ctx http.Context) http.Response {
uuid := ctx.param('uuid')
// Validate UUID format
if !is_valid_uuid(uuid) {
return ctx.bad_request('Invalid UUID format')
}
// Fetch user...
return ctx.ok('User')
}
fn is_valid_uuid(s string) bool {
// Simple UUID validation
return s.len == 36 && s[8] == `-` && s[13] == `-`
}
Method Routing
Handle multiple methods on same path:
fn main() {
mut web_app := varel_app.new('My App')
// Different handlers for different methods
web_app.get('/users/:id', show_user)!
web_app.put('/users/:id', update_user)!
web_app.delete('/users/:id', delete_user)!
web_app.listen(':8080')
}
Common Pitfalls
1. Parameter Name Conflicts
// ❌ BAD - Same parameter name at different positions
web_app.get('/users/:id', show_user)!
web_app.get('/posts/:id', show_post)!
// ✅ GOOD - Different parameter names
web_app.get('/users/:user_id', show_user)!
web_app.get('/posts/:post_id', show_post)!
2. Forgetting Error Propagation
// ❌ BAD - Missing !
web_app.get('/users', handler)
// ✅ GOOD - Propagate errors
web_app.get('/users', handler)!
3. Route Order
// ❌ BAD - General route before specific
web_app.get('/users/:id', show_user)!
web_app.get('/users/new', new_user)! // Never reached!
// ✅ GOOD - Specific before general
web_app.get('/users/new', new_user)!
web_app.get('/users/:id', show_user)!
Summary
You've learned:
✅ Basic routing with HTTP methods ✅ Route parameters and query strings ✅ Route groups for organization ✅ Middleware at global, group, and route levels ✅ Route priority and wildcards ✅ Performance characteristics (O(k) lookup) ✅ Advanced patterns and best practices
Continue to the Middleware Guide to learn about Varel's powerful middleware system!