| Crates.io | barnacle-rs |
| lib.rs | barnacle-rs |
| version | 0.3.0 |
| created_at | 2025-06-30 18:13:10.968262+00 |
| updated_at | 2025-07-24 08:02:04.103624+00 |
| description | Advanced rate limiting middleware for Axum with Redis backend, API key validation, and custom key extraction |
| homepage | https://github.com/zyphelabs/barnacle-rs |
| repository | https://github.com/zyphelabs/barnacle-rs |
| max_upload_size | |
| id | 1732156 |
| size | 3,401,140 |
Rate limiting and API key validation middleware for Axum with Redis backend.
Repository | Documentation | Crates.io
x-api-key header with per-key limits[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:
(String, ApiKeyConfig, Arc<Parts>, State).() for the last type parameter._ for the last type parameter to let Rust infer the closure type.# 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_test
For error handling and custom validator implementation, see:
examples/error_integration.rsexamples/custom_validator_example.rslet 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:
KeyExtractable implementationsStore 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.