Crates.io docs.rs License: MIT CI
HTTP for Rust services behind a reverse proxy. Does its job. Goes home.
Two dependencies — [matchit] for routing, tokio for async I/O. No hyper. No middleware stack you didn't ask for.
You're running behind nginx. nginx already handles TLS, rate limiting, slow clients, and body-size limits. Every general-purpose Rust HTTP framework re-implements all of that. astor doesn't — that's the whole point.
What changes between services is exactly what astor covers:
- Routing — radix tree, O(path-length),
{name}parameter syntax - Typed responses — named status codes, response shortcuts, typed builder
- Graceful shutdown — SIGTERM / Ctrl-C, drains in-flight requests before exit
# Cargo.toml [dependencies] astor = "0.3" tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
use astor::{Method, Request, Response, Router, Server, Status}; #[tokio::main] async fn main() { let app = Router::new() .on(Method::Get, "/users", list_users, ()) .on(Method::Get, "/users/{id}", get_user, ()) .on(Method::Post, "/users", create_user, ()) .on(Method::Delete, "/users/{id}", delete_user, ()); Server::bind("0.0.0.0:3000").serve(app).await.unwrap(); } // Path params: {name} syntax — req.param("id") → Option<&str> async fn get_user(req: Request) -> Response { let id = req.param("id").unwrap_or("unknown"); Response::json(format!(r#"{{"id":"{id}"}}"#).into_bytes()) } // Query params pre-parsed — req.query("key") → Option<&str> // GET /users?page=2&limit=10 still matches the /users route async fn list_users(req: Request) -> Response { let page = req.query("page").unwrap_or("1"); let limit = req.query("limit").unwrap_or("20"); Response::json(format!(r#"{{"page":{page},"limit":{limit},"users":[]}}"#).into_bytes()) } // req.body() → &[u8] — parse with serde_json, simd-json, or anything else async fn create_user(req: Request) -> Response { if req.body().is_empty() { return Response::status(Status::BadRequest); } Response::builder() .status(Status::Created) .header("location", "/users/99") .json(r#"{"id":"99"}"#.to_owned().into_bytes()) } // Return Status directly — astor wraps it into a response async fn delete_user(_req: Request) -> Status { Status::NoContent }
cargo run --example basic
curl http://localhost:3000/users/42
curl "http://localhost:3000/users?page=2&limit=10"Middleware runs as an ordered chain: global (router-level) first, then per-route extras, then the handler. The chain is baked into each route at startup — no runtime composition.
use astor::{Next, Request, Response, Router, Status}; async fn require_auth(req: Request, next: Next) -> Response { match req.header("authorization") { Some(_) => next.call(req).await, // proceed None => Response::status(Status::Unauthorized), // short-circuit } } async fn ownership_check(req: Request, next: Next) -> Response { // verify ownership, then proceed next.call(req).await }
Global middleware applies to every route registered after .middleware():
let users = Router::new() .middleware(require_auth) // applied to all routes below .on(Method::Get, "/users", list_users, ()) // chain: [require_auth] .on(Method::Put, "/users/{id}", update_user, ownership_check) // chain: [require_auth, ownership_check] .on(Method::Delete, "/users/{id}", delete_user, ownership_check);
Sub-router composition via .merge() — each sub-router keeps its own chain:
let public = Router::new().on(Method::Get, "/health", health, ()); let users = Router::new().middleware(require_auth).on(/* ... */); let products = Router::new().middleware(require_auth).on(/* ... */); let app = Router::new().merge(public).merge(users).merge(products); Server::bind("0.0.0.0:3000").serve(app).await.unwrap();
Pass () as the fourth argument to .on() when a route needs no extra middleware.
No free-form response constructor. No raw integers. Every status code is a named [Status] variant — tab-completable, greppable, compiler-verified.
Response::status(Status::NoContent) // 204 — not "204", not 204 Response::status(Status::NotFound) // 404 Response::builder() .status(Status::Created) .header("location", "/users/42") .json(bytes) // Return Status directly from a handler async fn delete_user(_req: Request) -> Status { Status::NoContent }
Every IANA-registered code from 100 to 511 — full list on docs.rs/astor.
Full API reference, nginx config, and Kubernetes deployment guide: docs.rs/astor
Contributions are welcome. Read CONTRIBUTING.md before opening a PR. See CHANGELOG.md for release history.
MIT