Rust in web and systems
The web and systems half of the Rust ecosystem is mature and stable. If you have written async server code in any language, the Rust version reads as you would expect, but with stronger guarantees about lifetime, concurrency, and error handling. The crates worth knowing are few.
The async runtime
| Crate | Role |
|---|---|
tokio | The async runtime. Almost everything in production async Rust uses it. |
futures | Combinators and utilities for Future and Stream. |
async-std / smol | Alternatives. Smaller communities. Almost never the right choice for new projects. |
Tokio's pieces you will see constantly:
tokio::spawnfor new tasks.tokio::sync::{mpsc, oneshot, broadcast, watch}for channels.tokio::sync::{Mutex, RwLock}for async-aware locks (different fromstd::sync).tokio::time::{sleep, timeout, interval}for scheduling.tokio::task::spawn_blockingfor CPU-bound work.
HTTP servers
| Crate | Role |
|---|---|
hyper | The HTTP implementation. Low level. Most servers build on top. |
axum | The current default. Ergonomic, type-safe, Tokio-native. |
actix-web | Mature alternative. Different design philosophy. |
tonic | gRPC. What Sail uses for Spark Connect. |
For new projects, the default is Axum for HTTP REST/JSON and Tonic for gRPC. Both compose with the rest of the Tokio ecosystem via the tower::Service trait.
A minimal Axum server gives you a sense of the shape:
use axum::{routing::get, Json, Router};
use serde::Serialize;
#[derive(Serialize)]
struct Health { ok: bool }
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let app = Router::new()
.route("/health", get(|| async { Json(Health { ok: true }) }));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
axum::serve(listener, app).await?;
Ok(())
}Three things to notice: #[tokio::main] sets up the runtime, handlers are plain async closures, serde::Serialize derives JSON output. There is no IoC container, no annotation magic, no surprise.
HTTP clients
| Crate | Role |
|---|---|
reqwest | The default ergonomic client. Built on hyper. |
ureq | Blocking, smaller, fewer features. Good for CLIs. |
For most projects, reqwest with tokio is the right answer. JSON via .json::<T>().await?, TLS via rustls (preferred over OpenSSL).
TLS
| Crate | Role |
|---|---|
rustls | Pure-Rust TLS implementation. Strongly preferred over OpenSSL bindings. |
tokio-rustls | The Tokio adapter. |
webpki-roots | Bundled trust anchors. |
Reach for rustls first. The OpenSSL binding (openssl-sys) has historically been a source of build pain and CVEs.
Serialization
| Crate | Role |
|---|---|
serde | The framework. Everything else builds on top. |
serde_json | JSON. The most-used serde format. |
prost | Protobuf. What Sail and most gRPC code uses. |
bincode, postcard, bson, toml, serde_yaml, csv | The other formats. Pick by need. |
Serde is the part of the Rust ecosystem that feels like magic. #[derive(Serialize, Deserialize)] on a struct produces correct, fast code for any format that implements the trait.
CLI
| Crate | Role |
|---|---|
clap | The argument parser. Derive-based or builder-based. |
indicatif | Progress bars. |
crossterm | Terminal control (colors, cursor). |
ratatui | TUI applications. |
For any CLI, start with clap derive macros:
use clap::Parser;
#[derive(Parser)]
struct Cli {
/// Input file path.
#[arg(short, long)]
input: std::path::PathBuf,
/// Verbose mode.
#[arg(short, long)]
verbose: bool,
}
fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
// ... cli.input, cli.verbose
Ok(())
}Database access
| Crate | Role |
|---|---|
sqlx | Compile-time-checked SQL. Postgres, MySQL, SQLite. |
sea-orm | Higher-level ORM. |
rusqlite | SQLite bindings, sync. |
diesel | Mature ORM with a different design philosophy. |
Most teams pick sqlx for new projects: typed queries, async, multiple backends, well-maintained.
Embedded and edge
| Crate | Role |
|---|---|
embassy | Async embedded Rust framework. |
no_std ecosystem | Crates that compile without the standard library. For microcontrollers. |
wasm-bindgen, wasmer-rs | WebAssembly. |
Most readers will not write embedded Rust. But knowing the ecosystem exists matters: if an agent suggests Rust for an edge device or a Wasm module, the path is well-paved.
What to look for as an orchestrator
When an agent writes web/systems Rust:
| Pattern | Watch for |
|---|---|
unwrap_or on parsed JSON | Hides invalid input. Use ? with a real error. |
tokio::spawn without JoinHandle | Fire-and-forget loses errors. |
reqwest without timeout | Default has no request timeout. Always set one. |
serde::Deserialize on user input without bounds | Maximum sizes, allowed values, sanitization. |
rustls vs openssl | Prefer rustls. Push back on OpenSSL bindings. |
| TLS verification disabled | danger_accept_invalid_certs(true) is a red flag outside tests. |
The ecosystem is wide but the surface you need for any one task is small. For most projects, tokio + axum + reqwest + sqlx + serde + clap + anyhow + thiserror + tracing is the entire dependency list. Lean on that.