Routing Guide

Routing Guide for Ignitia

⏱️ 9 min readπŸ“ 1631 wordsπŸ“… Updated 2025-10-16

Routing GuideπŸ”—

A comprehensive guide to routing in Ignitia - covering everything from basic route definition to advanced patterns and performance optimization.

Basic ConceptsπŸ”—

Ignitia’s routing system is built around the Router struct, which uses a compile-time optimized matching system for maximum performance. Routes are matched in order of specificity, with more specific routes taking precedence.

Route CompilationπŸ”—

Ignitia compiles routes at startup for optimal runtime performance:

use ignitia::{Router, Response};

let router = Router::new()
    .get("/", || async { Ok(Response::text("Home")) })
    .get("/about", || async { Ok(Response::text("About")) });

Route Matching PriorityπŸ”—

Routes are automatically sorted by specificity:

  1. Exact matches (e.g., /users/profile)
  2. Parameterized routes (e.g., /users/:id)
  3. Wildcard routes (e.g., /files/*path)

Route DefinitionπŸ”—

Basic Route RegistrationπŸ”—

use ignitia::{Router, Response, Result};

async fn home_handler() -> Result<Response> {
    Ok(Response::text("Welcome to Ignitia! πŸ”₯"))
}

async fn about_handler() -> Result<Response> {
    Ok(Response::html("<h1>About Us</h1>"))
}

let router = Router::new()
    .get("/", home_handler)
    .get("/about", about_handler);

Inline Route HandlersπŸ”—

let router = Router::new()
    .get("/", || async { Ok(Response::text("Home")) })
    .post("/submit", || async {
        Ok(Response::json(serde_json::json!({
            "status": "received"
        }))?)
    });

HTTP MethodsπŸ”—

Ignitia supports all standard HTTP methods with dedicated builder methods:

GET RoutesπŸ”—

let router = Router::new()
    .get("/users", list_users)
    .get("/users/:id", get_user)
    .get("/search", search_users);

POST RoutesπŸ”—

use ignitia::{Json, Response};
use serde::{Deserialize, Serialize};

#[derive(Deserialize)]
struct CreateUser {
    name: String,
    email: String,
}

#[derive(Serialize)]
struct UserResponse {
    id: u32,
    name: String,
    email: String,
}

let router = Router::new()
    .post("/users", |Json(user): Json<CreateUser>| async move {
        // Create user logic here
        let new_user = UserResponse {
            id: 1,
            name: user.name,
            email: user.email,
        };
        Ok(Response::json(new_user)?)
    });

PUT and PATCH RoutesπŸ”—

let router = Router::new()
    .put("/users/:id", |Path(id): Path<u32>, Json(user): Json<UpdateUser>| async move {
        // Full update logic
        Ok(Response::json(updated_user)?)
    })
    .patch("/users/:id", |Path(id): Path<u32>, Json(patch): Json<UserPatch>| async move {
        // Partial update logic
        Ok(Response::json(patched_user)?)
    });

DELETE RoutesπŸ”—

let router = Router::new()
    .delete("/users/:id", |Path(id): Path<u32>| async move {
        // Delete user logic
        Ok(Response::new(StatusCode::NO_CONTENT))
    });

HEAD and OPTIONS RoutesπŸ”—

let router = Router::new()
    .head("/users/:id", |Path(id): Path<u32>| async move {
        // Return headers only
        Ok(Response::new(StatusCode::OK))
    })
    .options("/users", || async {
        Ok(Response::new(StatusCode::OK)
            .header("Allow", "GET, POST, PUT, DELETE"))
    });

Path ParametersπŸ”—

Single ParametersπŸ”—

use ignitia::Path;

// Extract single parameter
let router = Router::new()
    .get("/users/:id", |Path(id): Path<u32>| async move {
        Ok(Response::text(format!("User ID: {}", id)))
    });

Multiple ParametersπŸ”—

// Extract multiple parameters as tuple
let router = Router::new()
    .get("/users/:user_id/posts/:post_id",
        |Path((user_id, post_id)): Path<(u32, u32)>| async move {
            Ok(Response::text(format!("User {} Post {}", user_id, post_id)))
        });

Named Parameter ExtractionπŸ”—

use serde::Deserialize;

#[derive(Deserialize)]
struct UserPost {
    user_id: u32,
    post_id: u32,
}

let router = Router::new()
    .get("/users/:user_id/posts/:post_id",
        |Path(params): Path<UserPost>| async move {
            Ok(Response::json(params)?)
        });

Optional Parameters with DefaultsπŸ”—

#[derive(Deserialize)]
struct PaginationParams {
    page: Option<u32>,
    limit: Option<u32>,
}

let router = Router::new()
    .get("/users/:id/posts", |Path(id): Path<u32>, Query(params): Query<PaginationParams>| async move {
        let page = params.page.unwrap_or(1);
        let limit = params.limit.unwrap_or(10);

        Ok(Response::json(serde_json::json!({
            "user_id": id,
            "page": page,
            "limit": limit
        }))?)
    });

Query ParametersπŸ”—

Basic Query ExtractionπŸ”—

use ignitia::Query;
use serde::Deserialize;

#[derive(Deserialize)]
struct SearchParams {
    q: String,
    category: Option<String>,
    sort: Option<String>,
}

let router = Router::new()
    .get("/search", |Query(params): Query<SearchParams>| async move {
        Ok(Response::json(serde_json::json!({
            "query": params.q,
            "category": params.category,
            "sort": params.sort.unwrap_or("relevance".to_string())
        }))?)
    });

Complex Query ParametersπŸ”—

#[derive(Deserialize)]
struct FilterParams {
    tags: Vec<String>,
    min_price: Option<f64>,
    max_price: Option<f64>,
    in_stock: Option<bool>,
}

let router = Router::new()
    .get("/products", |Query(filters): Query<FilterParams>| async move {
        // Filter products based on parameters
        Ok(Response::json(filtered_products)?)
    });

Route Groups and NestingπŸ”—

Basic Route GroupingπŸ”—

// Create API v1 routes
let api_v1 = Router::new()
    .get("/users", list_users_v1)
    .post("/users", create_user_v1)
    .get("/users/:id", get_user_v1);

// Create API v2 routes
let api_v2 = Router::new()
    .get("/users", list_users_v2)
    .post("/users", create_user_v2)
    .get("/users/:id", get_user_v2);

// Main router with nested routes
let router = Router::new()
    .get("/", home)
    .nest("/api/v1", api_v1)
    .nest("/api/v2", api_v2);

Nested Route GroupsπŸ”—

// Admin routes
let admin_routes = Router::new()
    .get("/dashboard", admin_dashboard)
    .get("/users", admin_users)
    .post("/users/:id/ban", ban_user);

// User management routes
let user_routes = Router::new()
    .get("/", list_users)
    .post("/", create_user)
    .get("/:id", get_user)
    .put("/:id", update_user)
    .delete("/:id", delete_user);

// Main application router
let app = Router::new()
    .get("/", home)
    .nest("/admin", admin_routes)
    .nest("/users", user_routes);

Shared State in Nested RoutesπŸ”—

use std::sync::Arc;

#[derive(Clone)]
struct AppState {
    db_pool: Arc<DatabasePool>,
    cache: Arc<Cache>,
}

let api_routes = Router::new()
    .get("/users", get_users)
    .post("/users", create_user)
    .state(app_state.clone());

let admin_routes = Router::new()
    .get("/stats", get_stats)
    .state(app_state.clone());

let app = Router::new()
    .nest("/api", api_routes)
    .nest("/admin", admin_routes);

Route MiddlewareπŸ”—

Per-Route MiddlewareπŸ”—

use ignitia::{LayeredHandler, AuthMiddleware, LoggerMiddleware};

let protected_handler = LayeredHandler::new(secret_handler)
    .layer(AuthMiddleware::new("secret-token"))
    .layer(LoggerMiddleware);

let router = Router::new()
    .get("/public", public_handler)
    .route_with_layered("/secret", Method::GET, protected_handler);

Route-Specific Middleware ChainsπŸ”—

use ignitia::middleware::{RateLimitingMiddleware, SecurityMiddleware};

// API routes with rate limiting
let api_handler = LayeredHandler::new(api_endpoint)
    .layer(RateLimitingMiddleware::per_minute(100))
    .layer(LoggerMiddleware);

// Admin routes with authentication and security
let admin_handler = LayeredHandler::new(admin_endpoint)
    .layer(AuthMiddleware::new("admin-token"))
    .layer(SecurityMiddleware::high_security())
    .layer(RateLimitingMiddleware::per_minute(10));

let router = Router::new()
    .route_with_layered("/api/data", Method::GET, api_handler)
    .route_with_layered("/admin/dashboard", Method::GET, admin_handler);

State ManagementπŸ”—

Application StateπŸ”—

use std::sync::Arc;
use tokio::sync::RwLock;

#[derive(Clone)]
struct AppState {
    counter: Arc<RwLock<u64>>,
    config: Arc<AppConfig>,
}

async fn increment_counter(State(state): State<AppState>) -> Result<Response> {
    let mut counter = state.counter.write().await;
    *counter += 1;
    Ok(Response::json(serde_json::json!({
        "counter": *counter
    }))?)
}

let app_state = AppState {
    counter: Arc::new(RwLock::new(0)),
    config: Arc::new(app_config),
};

let router = Router::new()
    .state(app_state)
    .get("/counter", increment_counter);

Database Connection PoolsπŸ”—

use sqlx::PgPool;

#[derive(Clone)]
struct DatabaseState {
    pool: PgPool,
}

async fn get_users(State(db): State<DatabaseState>) -> Result<Response> {
    let users = sqlx::query!("SELECT * FROM users")
        .fetch_all(&db.pool)
        .await
        .map_err(|e| Error::Database(e.to_string()))?;

    Ok(Response::json(users)?)
}

let db_state = DatabaseState {
    pool: PgPool::connect(&database_url).await?,
};

let router = Router::new()
    .state(db_state)
    .get("/users", get_users);

Advanced Routing PatternsπŸ”—

Wildcard RoutesπŸ”—

// Catch-all file serving
let router = Router::new()
    .get("/files/*path", |Path(path): Path<String>| async move {
        // Serve file from path
        let file_path = format!("./static/{}", path);
        serve_file(&file_path).await
    });

Route GuardsπŸ”—

async fn admin_only_guard(req: &Request) -> bool {
    req.header("x-admin-token").is_some()
}

// Custom route matching with guards
let router = Router::new()
    .get("/admin/*path", |req: Request| async move {
        if !admin_only_guard(&req).await {
            return Ok(Response::new(StatusCode::FORBIDDEN));
        }
        // Handle admin route
        Ok(Response::text("Admin area"))
    });

Dynamic Route RegistrationπŸ”—

let mut router = Router::new();

// Register routes dynamically
for endpoint in api_endpoints {
    router = router.get(&endpoint.path, endpoint.handler);
}

Route VersioningπŸ”—

// Version-aware routing
let router = Router::new()
    .get("/api/v1/users", users_v1)
    .get("/api/v2/users", users_v2)
    .get("/api/users", |headers: Headers| async move {
        let version = headers.get("API-Version").unwrap_or("v1");
        match version {
            "v2" => users_v2().await,
            _ => users_v1().await,
        }
    });

WebSocket RoutingπŸ”—

When the websocket feature is enabled, Ignitia supports WebSocket routing:

Basic WebSocket RoutesπŸ”—

use ignitia::websocket::{WebSocketConnection, Message};

let router = Router::new()
    .websocket("/ws", |ws: WebSocketConnection| async move {
        while let Some(msg) = ws.recv().await {
            match msg {
                Message::Text(text) => {
                    ws.send_text(format!("Echo: {}", text)).await?;
                }
                Message::Close(_) => break,
                _ => {}
            }
        }
        Ok(())
    });

WebSocket with StateπŸ”—

async fn chat_handler(
    ws: WebSocketConnection,
    State(chat_state): State<ChatState>
) -> Result<()> {
    // Handle chat WebSocket connection
    while let Some(msg) = ws.recv().await {
        if let Message::Text(text) = msg {
            // Broadcast to all connected clients
            chat_state.broadcast(text).await?;
        }
    }
    Ok(())
}

let router = Router::new()
    .state(chat_state)
    .websocket("/chat", chat_handler);

Performance ConsiderationsπŸ”—

Route CompilationπŸ”—

Ignitia compiles routes at startup for optimal performance:

// Routes are automatically sorted by specificity
let router = Router::new()
    .get("/users/:id/posts/:post_id", specific_handler)  // More specific
    .get("/users/:id/posts", less_specific_handler)      // Less specific
    .get("/users/:id", general_handler);                 // Most general

Route CachingπŸ”—

// Pre-compile regex patterns for path matching
let router = Router::new()
    .get("/users/:id", handler); // Regex compiled once at startup

Memory EfficiencyπŸ”—

// Use Arc for shared state to avoid cloning
#[derive(Clone)]
struct SharedState {
    data: Arc<RwLock<HashMap<String, String>>>,
}

let router = Router::new()
    .state_arc(Arc::new(shared_data)) // Use Arc directly
    .get("/data", get_data_handler);

Best PracticesπŸ”—

1. Route OrganizationπŸ”—

// Organize routes by feature/module
mod user_routes {
    use super::*;

    pub fn routes() -> Router {
        Router::new()
            .get("/", list_users)
            .post("/", create_user)
            .get("/:id", get_user)
            .put("/:id", update_user)
            .delete("/:id", delete_user)
    }
}

mod post_routes {
    use super::*;

    pub fn routes() -> Router {
        Router::new()
            .get("/", list_posts)
            .post("/", create_post)
            .get("/:id", get_post)
    }
}

// Main router assembly
let app = Router::new()
    .nest("/users", user_routes::routes())
    .nest("/posts", post_routes::routes());

2. Error HandlingπŸ”—

async fn safe_user_handler(Path(id): Path<u32>) -> Result<Response> {
    let user = get_user_by_id(id).await
        .map_err(|e| Error::Database(e.to_string()))?;

    match user {
        Some(user) => Ok(Response::json(user)?),
        None => Err(Error::NotFound(format!("User {} not found", id))),
    }
}

3. Input ValidationπŸ”—

use serde::Deserialize;

#[derive(Deserialize)]
struct CreateUserRequest {
    #[serde(deserialize_with = "validate_email")]
    email: String,
    #[serde(deserialize_with = "validate_password")]
    password: String,
}

async fn create_user_handler(Json(req): Json<CreateUserRequest>) -> Result<Response> {
    // Request is already validated by serde
    let user = create_user(req).await?;
    Ok(Response::json(user)?)
}

4. Route DocumentationπŸ”—

/// GET /users/:id
///
/// Retrieves a user by their unique identifier.
///
/// # Parameters
/// - `id`: The unique user ID (positive integer)
///
/// # Returns
/// - `200 OK`: User found and returned
/// - `404 Not Found`: User does not exist
/// - `400 Bad Request`: Invalid ID format
async fn get_user(Path(id): Path<u32>) -> Result<Response> {
    // Implementation
}

5. Testing RoutesπŸ”—

#[cfg(test)]
mod tests {
    use super::*;
    use ignitia::test::TestRequest;

    #[tokio::test]
    async fn test_get_user() {
        let router = create_test_router();

        let response = TestRequest::get("/users/1")
            .send(&router)
            .await;

        assert_eq!(response.status(), StatusCode::OK);
    }
}

This routing guide covers the essential patterns and advanced features of Ignitia’s routing system. For more specific examples and use cases, refer to the Examples documentation.