Skip to content

Navigation Menu

Sign in
Sign up

Latest commit

History

37 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

astor

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.


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]
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

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.


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 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


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

AltStyle によって変換されたページ (->オリジナル) /