Rate limiting and API key validation middleware for Axum with Redis backend.
Repository | Documentation | Crates.io
- Rate Limiting: IP-based or custom key-based rate limiting
- API Key Validation: Validate
x-api-keyheader with per-key limits - Request Modification: Modify request parts after validation but before processing
- Redis Backend: Distributed rate limiting with Redis
- Axum Middleware: Drop-in middleware for Axum applications
- Reset on Success: Optional rate limit reset on successful operations
- Extensible Design: Custom key stores and rate limiting strategies
[dependencies]
barnacle-rs = "0.3"
axum = "0.8"
tokio = { version = "1", features = ["full"] }use barnacle_rs::{RedisBarnacleStore, BarnacleConfig};
use axum::{Router, routing::get};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let store = RedisBarnacleStore::from_url("redis://127.0.0.1:6379").await?;
let config = BarnacleConfig {
max_requests: 10,
window: std::time::Duration::from_secs(60),
reset_on_success: barnacle_rs::ResetOnSuccess::Not,
};
let layer = barnacle_rs::BarnacleLayer::builder()
.with_store(store)
.with_config(config)
.build();
let app = Router::new()
.route("/api/data", get(handler))
.layer(layer);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
axum::serve(listener, app).await?;
Ok(())
}
async fn handler() -> &'static str {
"Hello, World!"
}use barnacle_rs::{BarnacleLayer, BarnacleConfig, RedisBarnacleStore, BarnacleError};
use axum::{Router, routing::get};
use std::sync::Arc;
use axum::http::request::Parts;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let store = RedisBarnacleStore::from_url("redis://127.0.0.1:6379").await?;
let config = BarnacleConfig::default();
let api_key_validator = |api_key: String, _api_key_config: ApiKeyConfig, _parts: Arc<Parts>, _state: ()| async move {
if api_key.is_empty() {
Err(BarnacleError::ApiKeyMissing)
} else if api_key != "test-key" {
Err(BarnacleError::invalid_api_key(api_key))
} else {
Ok(())
}
};
let layer: BarnacleLayer<(), RedisBarnacleStore, (), BarnacleError, _> = BarnacleLayer::builder()
.with_store(store)
.with_config(config)
.with_api_key_validator(api_key_validator)
.build()
.unwrap();
let app = Router::new()
.route("/api/protected", get(handler))
.layer(layer);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
axum::serve(listener, app).await?;
Ok(())
}
async fn handler() -> &'static str {
"Protected endpoint"
}use barnacle_rs::{BarnacleLayer, BarnacleConfig, RedisBarnacleStore, BarnacleError};
use axum::{Router, routing::get};
use std::sync::Arc;
use axum::http::request::Parts;
#[derive(Clone)]
struct MyState {
allowed_keys: Vec<String>,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let store = RedisBarnacleStore::from_url("redis://127.0.0.1:6379").await?;
let config = BarnacleConfig::default();
let state = MyState { allowed_keys: vec!["my-secret-key".to_string()] };
let api_key_validator = |api_key: String, _api_key_config: BarnacleConfig, _parts: Arc<Parts>, state: MyState| async move {
let allowed = state.allowed_keys.contains(&api_key);
if allowed {
Ok(())
} else {
Err(BarnacleError::invalid_api_key(api_key))
}
};
let layer: BarnacleLayer<(), RedisBarnacleStore, MyState, BarnacleError, _> = BarnacleLayer::builder()
.with_store(store)
.with_config(config)
.with_state(state)
.with_api_key_validator(api_key_validator)
.build()
.unwrap();
let app = Router::new()
.route("/api/protected", get(handler))
.layer(layer);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
axum::serve(listener, app).await?;
Ok(())
}
async fn handler() -> &'static str {
"Protected endpoint with state"
}use barnacle_rs::{KeyExtractable, BarnacleKey};
use axum::http::request::Parts;
#[derive(serde::Deserialize)]
struct LoginRequest {
email: String,
password: String,
}
impl KeyExtractable for LoginRequest {
fn extract_key(&self) -> BarnacleKey {
BarnacleKey::Email(self.email.clone())
}
}
let layer = barnacle_rs::BarnacleLayer::builder()
.with_store(store)
.with_config(config)
.build();let layer = barnacle_rs::BarnacleLayer::builder()
.with_store(store)
.with_config(config)
.build();let layer = barnacle_rs::BarnacleLayer::builder()
.with_store(api_key_store)
.with_config(config)
.build();use barnacle_rs::{KeyExtractable, BarnacleKey};
#[derive(serde::Deserialize)]
struct LoginRequest {
email: String,
password: String,
}
impl KeyExtractable for LoginRequest {
fn extract_key(&self) -> BarnacleKey {
BarnacleKey::Email(self.email.clone())
}
}
let layer = barnacle_rs::BarnacleLayer::builder()
.with_store(store)
.with_config(config)
.build();use barnacle_rs::{BarnacleLayer, RedisBarnacleStore, BarnacleError};
let middleware: BarnacleLayer<(), RedisBarnacleStore, (), BarnacleError, ()> = BarnacleLayer::builder()
.with_store(store)
.with_config(config)
.build()
.unwrap();use barnacle_rs::{BarnacleLayer, RedisBarnacleStore, BarnacleError};
use std::sync::Arc;
use axum::http::request::Parts;
let api_key_validator = |api_key: String, api_key_config: ApiKeyConfig, parts: Arc<Parts>, state: ()| async move {
if api_key == "test-key" {
Ok(())
} else {
Err(BarnacleError::invalid_api_key(api_key))
}
};
let middleware: BarnacleLayer<(), RedisBarnacleStore, (), BarnacleError, _> = BarnacleLayer::builder()
.with_store(store)
.with_config(config)
.with_api_key_validator(api_key_validator)
.with_state(())
.build()
.unwrap();Note:
- The validator closure must take owned arguments:
(String, ApiKeyConfig, Arc<Parts>, State). - If you do not provide a validator, use
()for the last type parameter. - If you provide a validator, use
_for the last type parameter to let Rust infer the closure type. - If you provide a request modifier, use
_for the last two type parameters to let Rust infer the types.
Modify request parts after validation but before the request reaches your handler:
use barnacle_rs::{BarnacleLayer, BarnacleConfig, RedisBarnacleStore, BarnacleError, ApiKeyConfig};
use axum::{Router, routing::get};
use axum::http::request::Parts;
use std::sync::Arc;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let store = RedisBarnacleStore::from_url("redis://127.0.0.1:6379").await?;
let config = BarnacleConfig::default();
// Optional API key validator
let api_key_validator = |api_key: String, _config: ApiKeyConfig, _parts: Arc<Parts>, _state: ()| async move {
if api_key == "valid-key" {
Ok(())
} else {
Err(BarnacleError::invalid_api_key(api_key))
}
};
// Request modifier that adds a custom header after validation
let request_modifier = |mut parts: Parts, _state: ()| async move {
parts.headers.insert(
"x-request-modified",
"true".parse().unwrap()
);
Ok(parts)
};
let layer: BarnacleLayer<(), RedisBarnacleStore, (), BarnacleError, _, _> = BarnacleLayer::builder()
.with_store(store)
.with_config(config)
.with_api_key_validator(api_key_validator) // Optional: only if you want validation
.with_request_modifier(request_modifier)
.build()
.unwrap();
let app = Router::new()
.route("/api/modified", get(handler))
.layer(layer);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
axum::serve(listener, app).await?;
Ok(())
}
async fn handler() -> &'static str {
"Request was modified by Barnacle!"
}Note:
- The modifier closure receives
(Parts, State)and returnsResult<Parts, Error>. - Modifications happen after successful validation but before request reconstruction.
- Use
_for the modifier type parameter to let Rust infer the closure type. - The modifier has access to all request parts (headers, extensions, URI, method, etc.).
- If you don't provide a modifier, use
()for the last type parameter.
# Run examples
cargo run --example basic
cargo run --example api_key_redis_test
cargo run --example custom_validator_example
cargo run --example error_integration
cargo run --example api_key_testFor error handling, custom validator implementation, and request modification, see:
examples/error_integration.rsexamples/custom_validator_example.rs
let config = BarnacleConfig {
max_requests: 100, // Requests per window
window: Duration::from_secs(3600), // Time window
reset_on_success: ResetOnSuccess::Yes( // Reset on success
Some(vec![200, 201]) // Status codes to reset on
),
};Barnacle automatically includes route information (path and method) in Redis keys, providing per-endpoint rate limiting without any additional configuration:
Redis Key Format:
barnacle:email:user@example.com:POST:/auth/login
barnacle:email:user@example.com:POST:/auth/start-reset
barnacle:api_keys:your-key:GET:/api/data
barnacle:ip:192.168.1.1:POST:/api/submit
This means:
- ✅ Same email can have different rate limits per endpoint
- ✅ No need to modify
KeyExtractableimplementations - ✅ Automatic separation of rate limits by route
- ✅ Backward compatible with existing code
Store API keys in Redis:
# Valid API key
redis-cli SET "barnacle:api_keys:your-key" 1
# Per-key rate limit config
redis-cli SET "barnacle:api_keys:config:your-key" '{"max_requests":100,"window":3600,"reset_on_success":"Not"}'MIT
Contributions are welcome! Please feel free to submit a Pull Request.
