Skip to content

Repository files navigation

astor

Crates.iodocs.rsLicense: MITCI

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.


The idea

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

Quick start

# Cargo.toml
[dependencies]
astor = "0.3"tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
use astor::{Method,Request,Response,Router,Server,Status};#[tokio::main]asyncfnmain(){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>asyncfnget_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 routeasyncfnlist_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 elseasyncfncreate_user(req:Request) -> Response{if req.body().is_empty(){returnResponse::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 responseasyncfndelete_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

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};asyncfnrequire_auth(req:Request,next:Next) -> Response{match req.header("authorization"){Some(_) => next.call(req).await,// proceedNone => Response::status(Status::Unauthorized),// short-circuit}}asyncfnownership_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.


Status codes are a type, not a number

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 204Response::status(Status::NotFound)// 404Response::builder().status(Status::Created).header("location","/users/42").json(bytes)// Return Status directly from a handlerasyncfndelete_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


Contributing

Contributions are welcome. Read CONTRIBUTING.md before opening a PR. See CHANGELOG.md for release history.


License

MIT

About

No description, website, or topics provided.

Resources

Contributing

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages