#HttpIngress Patterns Cookbook

Version: 0.36.0 | Updated: 2026-04-03 | Applies to: ranvier-http 0.36+ | Category: Cookbook


#Overview

HttpIngress is a circuit builder, not a web server. It wires Axon circuits to HTTP routes, manages Guards, and delegates to hyper for serving. This cookbook covers the most common patterns for routing, body parsing, and static assets.


#1. `post()` vs `post_typed::()` Decision Tree

flowchart TD
    Q{"Does the route accept a JSON request body?"}
    Q -->|NO| P["post()\nAxon‹(), Out, E, R›\nBody is ignored; circuit receives ()"]
    Q -->|YES| PT["post_typed::‹T›()\nAxon‹T, Out, E, R›\nBody is deserialized as T automatically\nT: DeserializeOwned + Serialize + JsonSchema"]

#`post()` -- No body

use ranvier_http::prelude::*;

let trigger_circuit = Axon::simple::<String>("trigger")
    .then(start_job);

Ranvier::http()
    .post("/api/jobs/start", trigger_circuit)
    .run(())
    .await?;

#`post_typed::()` -- Typed JSON body

use ranvier_http::prelude::*;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

#[derive(Debug, Deserialize, Serialize, JsonSchema)]
struct CreateOrder {
    product_id: String,
    quantity: u32,
    shipping_address: String,
}

let order_circuit = Axon::typed::<CreateOrder, String>("create-order")
    .then(validate_order)
    .then(persist_order);

Ranvier::http()
    .post_typed::<CreateOrder, _, _>("/api/orders", order_circuit)
    .run(())
    .await?;

post_typed automatically:

  • Parses the request body as JSON into T
  • Returns 400 Bad Request on parse failure
  • Records the JsonSchema for OpenAPI generation

The same pattern works for put_typed::<T>() and patch_typed::<T>().

#`schemars` Feature Requirements

JsonSchema derives require additional schemars features depending on the field types used in your request struct. If the derive fails to compile, consult this table:

Field Type Required Feature Cargo.toml Example
Uuid uuid1 schemars = { version = "1", features = ["derive", "uuid1"] }
NaiveDate, DateTime<Utc> chrono04 schemars = { version = "1", features = ["derive", "chrono04"] }
Decimal rust_decimal1 schemars = { version = "1", features = ["derive", "rust_decimal1"] }
Url url2 schemars = { version = "1", features = ["derive", "url2"] }

Multiple features can be combined: features = ["derive", "uuid1", "chrono04"].


#2. Path Parameter Extraction

Use :param syntax in the route path. Parameters are available via PathParams in the Bus.

use ranvier_http::prelude::*;
use ranvier_core::prelude::*;

let get_user = Axon::simple::<String>("get-user")
    .then_fn("extract-id", |_input, _res, bus| async move {
        let params = bus.require::<PathParams>();
        let user_id = params.get("id").unwrap_or("unknown");
        Outcome::next(format!("User: {}", user_id))
    });

Ranvier::http()
    .get("/api/users/:id", get_user)
    .get("/api/users/:id/posts/:slug", get_user_post)
    .run(())
    .await?;

Multiple parameters are supported: /api/orgs/:org_id/teams/:team_id.

#`PathParams::get()` Returns `&str`

PathParams::get() returns Option<&str>, not Option<&String>. When you need an owned String, use .map(|s| s.to_string()) instead of .cloned():

// Reading a path parameter as an owned String
let id: Option<String> = bus.get_cloned::<PathParams>().ok()
    .and_then(|params| params.get("id").map(|s| s.to_string()));

// Parsing a path parameter as Uuid
let uuid: Option<uuid::Uuid> = bus.get_cloned::<PathParams>().ok()
    .and_then(|params| params.get("id").and_then(|s| s.parse().ok()));

Migration note: If migrating from a HashMap<String, String> pattern, replace .cloned() with .map(|s| s.to_string()) since PathParams::get() returns &str, not &String.


#3. Bus Injector for Request Context

Use bus_injector() to extract HTTP request data into the Bus before any Guard or circuit runs. This is the escape hatch for data that built-in Guards do not extract.

use ranvier_http::prelude::*;
use ranvier_core::bus::Bus;

#[derive(Debug, Clone)]
struct UserAgent(String);

#[derive(Debug, Clone)]
struct AcceptLanguage(String);

Ranvier::http()
    .bus_injector(|parts: &http::request::Parts, bus: &mut Bus| {
        if let Some(ua) = parts.headers.get("user-agent") {
            if let Ok(s) = ua.to_str() {
                bus.insert(UserAgent(s.to_string()));
            }
        }
        if let Some(lang) = parts.headers.get("accept-language") {
            if let Ok(s) = lang.to_str() {
                bus.insert(AcceptLanguage(s.to_string()));
            }
        }
    })
    .get("/api/data", data_circuit)
    .run(())
    .await?;

Inside the circuit, read the injected values:

let circuit = Axon::simple::<String>("data")
    .then_fn("use-context", |_input, _res, bus| async move {
        let ua = bus.get_cloned::<UserAgent>()
            .map(|u| u.0)
            .unwrap_or_else(|_| "unknown".into());
        Outcome::next(format!("Hello from {}", ua))
    });

#4. Guard + Route Integration

Guards and routes compose naturally. Global Guards apply to all routes; per-route Guards apply to specific endpoints only.

use ranvier_http::{guards, prelude::*};
use ranvier_guard::prelude::*;

Ranvier::http()
    // Global: every request gets these
    .guard(RequestIdGuard::new())
    .guard(AccessLogGuard::new())
    .guard(CorsGuard::<()>::permissive())
    // Read endpoints: no extra guards
    .get("/api/products", list_products)
    .get("/api/products/:id", get_product)
    // Write endpoints: per-route guards
    .post_with_guards("/api/products", create_product, guards![
        ContentTypeGuard::json(),
        RequestSizeLimitGuard::max_2mb(),
    ])
    .put_with_guards("/api/products/:id", update_product, guards![
        ContentTypeGuard::json(),
    ])
    .delete("/api/products/:id", delete_product)
    .run(())
    .await?;

#5. Static Assets with `serve_dir()`

Mount a directory to serve static files. Ranvier handles MIME type detection, 304 caching, directory indexes, and pre-compressed variants.

Use this when one process should serve both API routes and a built frontend. For pure static hosting or CDN-first asset delivery, prefer nginx, Caddy, object storage/CDN, or tower-http::ServeDir.

#Basic Setup

use ranvier_http::prelude::*;

Ranvier::http()
    .serve_dir("/static", "./public")
    .get("/api/data", data_circuit)
    .run(())
    .await?;

#SPA with Fallback

Ranvier::http()
    .serve_dir("/", "./dist")
    .directory_index("index.html")
    .spa_fallback("./dist/index.html")
    .run(())
    .await?;

#Production Static Assets

Ranvier::http()
    .serve_dir("/assets", "./build/assets")
    .directory_index("index.html")
    .static_cache_control("public, max-age=3600")
    .immutable_cache()                  // hashed filenames get max-age=31536000
    .serve_precompressed()              // check for .br / .gz variants first
    .enable_range_requests()            // support Range: bytes=X-Y (206 responses)
    .get("/api/data", data_circuit)
    .run(())
    .await?;

immutable_cache() detects filenames with content hashes (e.g., app.a1b2c3.js) and applies Cache-Control: public, max-age=31536000, immutable.

serve_precompressed() checks for .br and .gz pre-compressed variants of static files before serving the original. This pairs well with build tools that generate pre-compressed output.

enable_range_requests() adds support for partial content responses (HTTP 206), useful for large file downloads and media streaming.


#6. Health Endpoints

use ranvier_http::prelude::*;

Ranvier::http()
    .health_endpoint("/health")
    .readiness_liveness("/ready", "/live")
    .health_check("database", |resources| async move {
        // Your health check logic
        Ok(())
    })
    .get("/api/data", data_circuit)
    .run(())
    .await?;

Or use the default paths (/health/ready, /health/live):

Ranvier::http()
    .readiness_liveness_default()
    .run(())
    .await?;

#7. WebSocket Routes

use ranvier_http::prelude::*;

Ranvier::http()
    .ws("/ws/chat", |mut conn, _resources, _bus| async move {
        conn.send(WebSocketEvent::text("Welcome!")).await.ok();
        while let Ok(Some(msg)) = conn.next_json::<serde_json::Value>().await {
            let echo = WebSocketEvent::json(&msg).unwrap();
            if conn.send(echo).await.is_err() {
                break;
            }
        }
    })
    .run(())
    .await?;

#8. Error Handlers

Register custom error-to-response converters for specific routes:

use ranvier_http::prelude::*;

Ranvier::http()
    .post_with_error("/api/orders", order_circuit, |error| {
        match error.to_string().as_str() {
            e if e.contains("duplicate") => (409, "Conflict: duplicate order"),
            e if e.contains("invalid") => (400, "Bad Request: invalid input"),
            _ => (500, "Internal Server Error"),
        }
    })
    .run(())
    .await?;

#See Also

  • Guard Patterns Cookbook -- Guard composition patterns
  • Bus Access Patterns -- Bus read/write decision guide
  • Deployment Guide -- production deployment configuration