Camber is opinionated async Rust for IO-bound services on top of Tokio.
In development, publicly usable, and actively dogfooded.
Camber is a library and project tool for the large middle of Rust services that are IO-bound, not scheduler experiments.
cargo add cambercargo install camber-cliBuild HTTP services without extractors, Tower, or #[tokio::main]. Async handlers on a Tokio core.
cargo install camber-cli
camber new my-service --template http
cd my-service
cargo runuse camber::RuntimeError;
use camber::http::{self, Response, Router};
fn main() -> Result<(), RuntimeError> {
let mut router = Router::new();
router.get("/hello", |_req| async { Response::text(200, "Hello, world!") });
http::serve("0.0.0.0:8080", router)
}Use http::serve(...) by itself for the default case. Wrap it in runtime::builder().run(...) only when you need runtime configuration such as worker counts, shutdown timeouts, or registered resources.
- Vision
- Tokio/Axum to Camber
- Go to Camber
- Proxy Quickstart
- Cross-Compilation
- Reference
- Runtime Reference
- HTTP Reference
- Middleware Reference
- HTTP Client Reference
- Tasks and Channels Reference
- Error Reference
- Config Reference
- TLS Reference
- Net Reference
- Resource Reference
- Scheduling Reference
- Secret Reference
- Signals and Shutdown Reference
- Time Reference
- Logging Reference
If you're evaluating Camber as a library, start with Tokio/Axum to Camber or the Reference.
The README is the overview. docs/reference/ and docs.rs are the exhaustive public surface.
Config-driven reverse proxy with auto-TLS and health checks. Suited for homelab and internal deployments. Not yet a production edge replacement.
cargo install camber-cli
camber serve config.tomllisten = "0.0.0.0:443"
[tls]
auto = true
email = "admin@example.com"
[[site]]
host = "jellyfin.example.com"
proxy = "http://192.168.1.10:8096"
[[site]]
host = "immich.example.com"
proxy = "http://192.168.1.10:2283"
[[site]]
host = "grafana.example.com"
proxy = "http://192.168.1.10:3000"
health_check = "/api/health"Full setup guide: Proxy Quickstart
use camber::http::{cors, compression, rate_limit};
router.use_middleware(cors::allow_origins(&["https://app.example.com"]));
router.use_middleware(compression::auto());
router.use_middleware(rate_limit::per_second(100)?);Auth is just middleware:
use camber::http::{Response, IntoResponse};
router.use_middleware(|req, next| {
let authorized = req.header("authorization").is_some_and(valid);
async move {
match authorized {
true => next.call(req).await,
false => Response::text(401, "unauthorized")?.into_response(),
}
}
});If middleware needs request data after .await, copy out owned data before entering async move.
For normal HTTP handlers, middleware wraps the full owned Request and Response.
For gRPC and proxy_stream, middleware acts as a request gate before streaming begins.
router.ws("/chat", |req, mut conn: WsConn| {
while let Some(msg) = conn.recv() {
conn.send(&format!("echo: {msg}"))?;
}
Ok(())
});router.get_sse("/events", |_req, sse| {
sse.event("update", r#"{"status":"ok"}"#)?;
Ok(())
});For generic chunked responses, use StreamResponse::new() for the default buffer or
StreamResponse::with_buffer(status, cap) when you need explicit backpressure tuning.
use camber::http;
let resp = http::get("https://api.example.com/data").await?;
let resp = http::post_json("https://api.example.com/items", &body).await?;
let resp = http::put("https://api.example.com/items/1", &body).await?;
let resp = http::delete("https://api.example.com/items/1").await?;
let resp = http::patch_json("https://api.example.com/items/1", &body).await?;
let resp = http::client().retries(3).get("https://flaky-api.example.com/data").await?;let session = req.cookie("session_id");
let resp = Response::text(200, "ok")?.set_cookie("session_id", "abc123");router.post("/upload", |req| async {
let multipart = req.multipart()?;
for part in multipart.parts() {
save(part.filename(), part.data());
}
Response::text(200, "uploaded")?
});use sqlx::PgPool;
let pool = PgPool::connect("postgres://localhost/mydb").await?;
// In a handler:
let user = sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = $1")
.bind(id)
.fetch_one(&pool)
.await?;Camber does not wrap database drivers. Use sqlx or your preferred ORM directly inside handlers and background tasks.
use camber::http::otel;
use camber::circuit_breaker;
router.use_middleware(otel::tracing());
let protected = circuit_breaker::wrap(pool)
.failure_threshold(3)
.cooldown(Duration::from_secs(30))
.build();Dual-licensed under MIT and Apache 2.0.