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

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.v inside the register_routes() function
  • Controllers go in controllers/ directory (one file per resource)
  • The examples below show the routing code only - adapt them for your routes.v file

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)

  1. Exact static matches

    web_app.get('/users/new', handler)!  // Highest priority
    
  2. Static prefixes with parameters

    web_app.get('/users/:id', handler)!  // Medium priority
    
  3. Catchall/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

  1. Use Static Paths When Possible

    // ✅ Fast - static lookup
    web_app.get('/api/users', handler)!
    
    // ❌ Slower - dynamic lookup
    web_app.get('/api/:resource', handler)!
    
  2. 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)!
    
  3. Group Related Routes

    // ✅ Efficient - shared prefix
    api := web_app.group('/api/v1')
    api.get('/users', users)!
    api.get('/posts', posts)!
    
  4. 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!