#Guard Patterns Cookbook
Version: 0.36.0 | Updated: 2026-03-20 | Applies to: ranvier-guard 0.36+ | Category: Cookbook
#Overview
Guards are typed Transition<T, T> nodes that enforce security and policy constraints
as visible, traceable pipeline steps. Instead of hidden Tower middleware layers, Guards
are first-class pipeline citizens that read from and write to the Bus.
This cookbook covers the most common Guard composition patterns.
#1. Global Guard Registration
Register Guards on HttpIngress with .guard(). Global Guards run on every request,
in registration order, before any route circuit executes.
use ranvier_http::prelude::*;
use ranvier_guard::prelude::*;
Ranvier::http()
.guard(RequestIdGuard::new())
.guard(AccessLogGuard::new())
.guard(CorsGuard::new(CorsConfig {
allowed_origins: vec!["https://app.example.com".into()],
allowed_methods: vec!["GET".into(), "POST".into(), "PUT".into(), "DELETE".into()],
allowed_headers: vec!["Content-Type".into(), "Authorization".into()],
max_age_seconds: 86400,
allow_credentials: true,
}))
.guard(SecurityHeadersGuard::new(SecurityPolicy::default()))
.guard(CompressionGuard::new().prefer_brotli())
.get("/api/health", health_circuit)
.run(())
.await?;Execution order: RequestIdGuard -> AccessLogGuard -> CorsGuard -> SecurityHeadersGuard -> CompressionGuard -> route circuit.
#2. Per-Route Guards with `guards![]`
Use post_with_guards() (or get_with_guards, put_with_guards, etc.) with the
guards![] macro to apply Guards to specific routes only. Per-route Guards execute
after global Guards.
use ranvier_http::{guards, prelude::*};
use ranvier_guard::prelude::*;
Ranvier::http()
.guard(AccessLogGuard::new()) // global — all routes
.guard(CorsGuard::<()>::permissive()) // global — all routes
.post_with_guards("/api/orders", order_circuit, guards![
ContentTypeGuard::json(), // per-route — POST only
IdempotencyGuard::ttl_5min(), // per-route — POST only
RequestSizeLimitGuard::max_2mb(), // per-route — POST only
])
.get("/api/orders", list_circuit) // no extra guards
.run(())
.await?;The guards![] macro calls GuardIntegration::register() on each guard expression,
producing a Vec<RegisteredGuard>.
#3. Custom Guard Implementation
Any Transition<T, T> can serve as a Guard. The pattern is straightforward: read
context from the Bus, validate, then either pass the input through (Outcome::next)
or reject it (Outcome::fault).
use async_trait::async_trait;
use ranvier_core::prelude::*;
use serde::{Deserialize, Serialize};
/// Bus type: API version extracted from the Accept header.
#[derive(Debug, Clone)]
pub struct ApiVersion(pub String);
/// Guard that rejects requests without a supported API version.
#[derive(Debug, Clone)]
pub struct ApiVersionGuard<T> {
supported: Vec<String>,
_marker: std::marker::PhantomData<T>,
}
impl<T> ApiVersionGuard<T> {
pub fn new(supported: Vec<String>) -> Self {
Self {
supported,
_marker: std::marker::PhantomData,
}
}
}
#[async_trait]
impl<T: Send + Sync + 'static> Transition<T, T> for ApiVersionGuard<T> {
type Error = String;
type Resources = ();
async fn run(
&self,
input: T,
_resources: &Self::Resources,
bus: &mut Bus,
) -> Outcome<T, Self::Error> {
let version = bus.get_cloned::<ApiVersion>()
.map(|v| v.0)
.unwrap_or_default();
if self.supported.contains(&version) {
Outcome::next(input)
} else {
Outcome::fault(format!(
"406 Not Acceptable: API version '{}' not supported",
version
))
}
}
}#4. Guard Bus Injection
Guards communicate through the Bus. The GuardIntegration trait automatically wires
HTTP headers into Bus types and Bus types back into response headers.
Read flow: HTTP Request -> BusInjectorFn -> Bus -> Guard reads Write flow: Guard writes -> Bus -> ResponseExtractorFn -> HTTP Response
Built-in Bus wiring examples:
| Guard | Reads from Bus | Writes to Bus |
|---|---|---|
CorsGuard |
RequestOrigin |
CorsHeaders |
AuthGuard |
AuthorizationHeader |
IamIdentity |
RateLimitGuard |
ClientIdentity |
-- |
CompressionGuard |
AcceptEncoding |
CompressionConfig |
RequestIdGuard |
RequestId (optional) |
RequestId |
ContentTypeGuard |
RequestContentType |
-- |
TimeoutGuard |
-- | TimeoutDeadline |
IdempotencyGuard |
IdempotencyKey |
IdempotencyCachedResponse |
#Production `bus_injector` Pattern
When using Guards alongside application data, inject the database pool,
PathParams, and request headers in a single bus_injector closure:
use ranvier_http::{PathParams, prelude::*};
use ranvier_guard::prelude::*;
use sqlx::PgPool;
let pool = PgPool::connect(&database_url).await?;
Ranvier::http()
.bind("0.0.0.0:8080")
.bus_injector({
let pool = pool.clone();
move |parts: &http::request::Parts, bus: &mut Bus| {
// Application data
bus.insert(pool.clone());
// Path parameters (auto-extracted by HttpIngress)
if let Some(params) = parts.extensions.get::<PathParams>() {
bus.insert(params.clone());
}
// Request headers as Vec<(String, String)>
let headers: Vec<(String, String)> = parts.headers.iter()
.map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
.collect();
bus.insert(headers);
}
})
// Guards auto-inject their own Bus types (RequestOrigin, etc.)
.guard(CorsGuard::<()>::permissive())
.guard(AccessLogGuard::new())
.post_typed("/api/orders", order_pipeline())
.get("/api/orders/:id", get_order_pipeline())
.run(())
.await?;Guard Bus types (RequestOrigin, CorsHeaders, etc.) are injected automatically
by the GuardIntegration trait -- you only need to inject application-specific
data in bus_injector.
#5. Authentication Guard Patterns
#Bearer Token
use ranvier_guard::prelude::*;
// Simple token validation
let auth = AuthGuard::bearer(vec![
std::env::var("API_TOKEN").expect("API_TOKEN must be set"),
]);
Ranvier::http()
.guard(auth)
.get("/api/protected", protected_circuit)
.run(())
.await?;#Bearer + Role Policy
use ranvier_core::iam::IamPolicy;
use ranvier_guard::prelude::*;
let admin_auth = AuthGuard::bearer(vec!["admin-token".into()])
.with_policy(IamPolicy::RequireRole("admin".into()));
Ranvier::http()
.get_with_guards("/api/admin", admin_circuit, guards![admin_auth])
.run(())
.await?;#Custom JWT Validator
use ranvier_core::iam::IamIdentity;
use ranvier_guard::prelude::*;
let jwt_auth = AuthGuard::custom(|header_value| {
let token = header_value
.strip_prefix("Bearer ")
.ok_or("expected Bearer scheme")?;
// Your JWT validation logic here
let claims = validate_jwt(token).map_err(|e| e.to_string())?;
Ok(IamIdentity::new(&claims.sub).with_role(&claims.role))
});#6. Real-World Example: API Server
Combining CORS + Auth + RateLimit + Compression + Logging for a production API:
use ranvier_http::{guards, prelude::*};
use ranvier_guard::prelude::*;
use std::time::Duration;
let api_token = std::env::var("API_TOKEN").expect("API_TOKEN required");
Ranvier::http()
.bind("0.0.0.0:8080")
// --- Global Guards (all routes) ---
.guard(RequestIdGuard::new())
.guard(AccessLogGuard::new().redact_paths(vec!["/auth/login".into()]))
.guard(CorsGuard::new(CorsConfig {
allowed_origins: vec!["https://app.example.com".into()],
..Default::default()
}))
.guard(SecurityHeadersGuard::new(
SecurityPolicy::default().with_csp("default-src 'self'"),
))
.guard(RateLimitGuard::new(100, 60_000)) // 100 req/min
.guard(CompressionGuard::new().prefer_brotli())
// --- Auth-protected routes ---
.get_with_guards("/api/users", list_users, guards![
AuthGuard::bearer(vec![api_token.clone()]),
])
.post_with_guards("/api/users", create_user, guards![
AuthGuard::bearer(vec![api_token.clone()]),
ContentTypeGuard::json(),
RequestSizeLimitGuard::max_2mb(),
IdempotencyGuard::ttl_5min(),
])
// --- Public routes (no auth guard) ---
.get("/health", health_circuit)
.run(())
.await?;#7. Guard Error Responses
Guards encode HTTP status codes in their error messages. The GuardIntegration
layer parses "NNN " prefixes from Fault messages and sets the corresponding
response status code:
CorsGuard-> 403 ForbiddenAuthGuard-> 401 Unauthorized / 403 Forbidden (policy failure)RateLimitGuard-> 429 Too Many RequestsRequestSizeLimitGuard-> 413 Payload Too LargeContentTypeGuard-> 415 Unsupported Media TypeTimeoutGuard-> 408 Request Timeout
If a Guard's Fault message starts with a 3-digit status code (e.g., "413 Payload Too Large"),
that code is used as the HTTP response status. Otherwise, the Guard's default_status applies.
#See Also
- Bus Access Patterns -- Bus read/write decision guide
- HttpIngress Patterns Cookbook -- route registration patterns
- Security Hardening Guide -- production security checklist