Web Frameworks in Rust
Rust is production-ready for web backends. Companies including Cloudflare, Discord, Dropbox, and AWS run Rust web services handling millions of requests per second. The ecosystem has matured significantly: there are stable, ergonomic frameworks, async HTTP clients, and both synchronous and asynchronous database drivers.
Rust web services benefit from the same guarantees as the rest of the language — memory safety without a garbage collector, fearless concurrency, and predictable latency with no GC pauses.
axum
axum is built by the tokio team, layered on top of hyper (an HTTP implementation) and tower (a composable middleware stack). It offers ergonomic routing, a powerful extractor pattern for typed request parsing, and integrates naturally with the rest of the tokio ecosystem.
[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"use axum::{Router, routing::get};
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(root_handler))
.route("/hello", get(hello_handler));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
println!("listening on http://localhost:3000");
axum::serve(listener, app).await.unwrap();
}
async fn root_handler() -> &'static str {
"Welcome to the API"
}
async fn hello_handler() -> &'static str {
"Hello, world!"
}Extractors and JSON responses
axum's extractor pattern lets handler functions declare what they need from the request as typed function parameters. The framework automatically parses and validates the input before the handler runs.
use axum::{
Router,
routing::{get, post},
extract::{Path, Json},
http::StatusCode,
};
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
struct CreateUser {
name: String,
email: String,
}
#[derive(Serialize)]
struct UserResponse {
id: u64,
name: String,
email: String,
}
// Extract a path parameter
async fn get_user(Path(id): Path<u64>) -> Json<UserResponse> {
Json(UserResponse {
id,
name: String::from("Alice"),
email: String::from("alice@example.com"),
})
}
// Extract a JSON request body
async fn create_user(
Json(payload): Json<CreateUser>,
) -> (StatusCode, Json<UserResponse>) {
let user = UserResponse {
id: 42,
name: payload.name,
email: payload.email,
};
(StatusCode::CREATED, Json(user))
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/users/:id", get(get_user))
.route("/users", post(create_user));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}Path, Query, Json, and a customExtension for database state, all in the same function signature.actix-web
actix-web is one of the fastest web frameworks in any language and regularly tops TechEmpower benchmarks. It is mature and battle-tested with a large ecosystem of middleware and extensions. Originally built on the Actix actor framework, the API is now straightforward and does not require you to use actors.
[dependencies]
actix-web = "4"
serde = { version = "1", features = ["derive"] }
serde_json = "1"use actix_web::{web, App, HttpServer, HttpResponse, Responder};
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
struct Greeting {
message: String,
}
async fn hello() -> impl Responder {
HttpResponse::Ok().json(Greeting {
message: String::from("Hello from actix-web!"),
})
}
async fn echo(body: web::Json<Greeting>) -> impl Responder {
HttpResponse::Ok().json(Greeting {
message: format!("You said: {}", body.message),
})
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.route("/hello", web::get().to(hello))
.route("/echo", web::post().to(echo))
})
.bind("127.0.0.1:8080")?
.run()
.await
}Rocket
Rocket focuses on developer ergonomics and safety. It uses attribute macros to declare routes, provides type-safe request guards, and gives clear compile-time errors when route handlers are misconfigured. Rocket requires nightly Rust for some features but is stable on stable Rust as of version 0.5.
[dependencies]
rocket = "0.5"
serde = { version = "1", features = ["derive"] }#[macro_use] extern crate rocket;
use rocket::serde::{json::Json, Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
struct Message {
content: String,
}
#[get("/hello/<name>")]
fn hello(name: &str) -> String {
format!("Hello, {}!", name)
}
#[post("/echo", data = "<msg>")]
fn echo(msg: Json<Message>) -> Json<Message> {
msg
}
#[launch]
fn rocket() -> _ {
rocket::build()
.mount("/", routes![hello, echo])
}Framework Comparison
Framework | Speed | Ergonomics | Ecosystem | Async | Best For |
|---|---|---|---|---|---|
axum | Very fast | Excellent | Large (tower) | tokio-native | New projects, tokio ecosystem |
actix-web | Fastest | Good | Largest | actix-native | Maximum throughput, large apps |
Rocket | Fast | Best-in-class | Medium | Yes (0.5+) | Developer experience, rapid prototyping |
warp | Very fast | Functional/composable | Medium | tokio-native | Filter composition, smaller APIs |
Database Access
Rust has a mature database ecosystem covering both synchronous and async use cases.
Crate | Style | Databases | Notes |
|---|---|---|---|
sqlx | Async | PostgreSQL, MySQL, SQLite | Compile-time SQL verification; most popular async choice |
sea-orm | Async ORM | PostgreSQL, MySQL, SQLite | Built on sqlx; ActiveRecord-style API |
diesel | Sync ORM | PostgreSQL, MySQL, SQLite | Powerful type-safe query builder; synchronous |
rusqlite | Sync | SQLite only | Lightweight; good for embedded databases |
# sqlx with PostgreSQL support
[dependencies]
sqlx = { version = "0.7", features = ["runtime-tokio", "postgres", "macros"] }use sqlx::PgPool;
#[derive(sqlx::FromRow, serde::Serialize)]
struct User {
id: i64,
name: String,
email: String,
}
async fn get_user(pool: &PgPool, id: i64) -> Result<User, sqlx::Error> {
// query! macro verifies SQL against the database at compile time
let user = sqlx::query_as::<_, User>(
"SELECT id, name, email FROM users WHERE id = $1"
)
.bind(id)
.fetch_one(pool)
.await?;
Ok(user)
}
#[tokio::main]
async fn main() -> Result<(), sqlx::Error> {
let pool = PgPool::connect("postgres://user:pass@localhost/mydb").await?;
let user = get_user(&pool, 1).await?;
println!("found: {} <{}>", user.name, user.email);
Ok(())
}HTTP Client: reqwest
reqwest is the most popular async HTTP client for Rust. It is built on tokio and hyper, supports JSON serialization via serde, handles cookies, redirects, TLS, and streaming responses.
[dependencies]
reqwest = { version = "0.11", features = ["json"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }use serde::Deserialize;
#[derive(Deserialize, Debug)]
struct Todo {
id: u32,
title: String,
completed: bool,
}
#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
// GET request, deserialize JSON response
let todo: Todo = reqwest::get(
"https://jsonplaceholder.typicode.com/todos/1"
)
.await?
.json()
.await?;
println!("todo #{}: {}", todo.id, todo.title);
println!("completed: {}", todo.completed);
Ok(())
}Complete axum Example with Error Handling
A more realistic axum application with a shared database pool, typed error responses, and structured JSON output.
use axum::{
Router,
routing::{get, post},
extract::{Path, State, Json},
http::StatusCode,
response::{IntoResponse, Response},
};
use serde::{Serialize, Deserialize};
use std::sync::{Arc, Mutex};
use std::collections::HashMap;
// ---- Types ----
#[derive(Clone, Serialize, Deserialize)]
struct User {
id: u64,
name: String,
email: String,
}
#[derive(Deserialize)]
struct CreateUser {
name: String,
email: String,
}
// Shared application state
#[derive(Clone)]
struct AppState {
users: Arc<Mutex<HashMap<u64, User>>>,
next_id: Arc<Mutex<u64>>,
}
// ---- Error type ----
enum AppError {
NotFound(String),
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let (status, message) = match self {
AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg),
};
(status, Json(serde_json::json!({ "error": message }))).into_response()
}
}
// ---- Handlers ----
async fn list_users(State(state): State<AppState>) -> Json<Vec<User>> {
let users = state.users.lock().unwrap();
Json(users.values().cloned().collect())
}
async fn get_user(
Path(id): Path<u64>,
State(state): State<AppState>,
) -> Result<Json<User>, AppError> {
let users = state.users.lock().unwrap();
users.get(&id)
.cloned()
.map(Json)
.ok_or_else(|| AppError::NotFound(format!("user {} not found", id)))
}
async fn create_user(
State(state): State<AppState>,
Json(payload): Json<CreateUser>,
) -> (StatusCode, Json<User>) {
let mut next_id = state.next_id.lock().unwrap();
let id = *next_id;
*next_id += 1;
let user = User { id, name: payload.name, email: payload.email };
state.users.lock().unwrap().insert(id, user.clone());
(StatusCode::CREATED, Json(user))
}
// ---- Main ----
#[tokio::main]
async fn main() {
let state = AppState {
users: Arc::new(Mutex::new(HashMap::new())),
next_id: Arc::new(Mutex::new(1)),
};
let app = Router::new()
.route("/users", get(list_users).post(create_user))
.route("/users/:id", get(get_user))
.with_state(state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
println!("API running at http://localhost:3000");
axum::serve(listener, app).await.unwrap();
}Middleware with tower
axum uses tower layers for middleware, giving you access to a huge library of battle-tested middleware for logging, tracing, rate limiting, compression, CORS, and authentication.
[dependencies]
axum = "0.7"
tower-http = { version = "0.5", features = ["cors", "trace", "compression-gzip"] }
tracing-subscriber = "0.3"use axum::Router;
use tower_http::{
cors::CorsLayer,
trace::TraceLayer,
compression::CompressionLayer,
};
#[tokio::main]
async fn main() {
// Initialize tracing
tracing_subscriber::fmt::init();
let app = Router::new()
// ... your routes ...
.layer(TraceLayer::new_for_http()) // structured request logging
.layer(CompressionLayer::new()) // gzip/brotli responses
.layer(CorsLayer::permissive()); // CORS headers
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}Deployment
One of Rust's biggest deployment advantages is that cargo build --release produces a
single, self-contained binary with no runtime dependency on an interpreter or VM.
cargo build --release— produces a heavily optimised binary intarget/release/.The binary is typically 5–20 MB and starts in milliseconds with no warm-up.
For the smallest Docker images, compile with the musl target to get a fully static binary, then use a
scratchordistrolessbase image.Cross-compilation with the
crosscrate handles building for Linux from macOS or Windows with a single command.
# Standard release build cargo build --release # Static Linux binary (for Docker scratch images) rustup target add x86_64-unknown-linux-musl cargo build --release --target x86_64-unknown-linux-musl # Cross-compile from any host using the 'cross' tool cargo install cross cross build --release --target x86_64-unknown-linux-musl
# Multi-stage Dockerfile for a minimal image FROM rust:1.78 AS builder WORKDIR /app COPY . . RUN cargo build --release --target x86_64-unknown-linux-musl # Final image — no OS, just the binary FROM scratch COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/my_server / ENTRYPOINT ["/my_server"]
scratch container has essentially zero attack surface — there is no shell, no package manager, and no libc to exploit.Ecosystem at a Glance
axum — ergonomic, tokio-native, tower middleware; recommended for new projects.
actix-web — highest raw throughput; large ecosystem; good choice for large-scale APIs.
Rocket — best developer experience; attribute macros, type-safe guards.
sqlx — async SQL with compile-time verified queries.
sea-orm — async ORM built on sqlx; Active Record-style API.
diesel — synchronous, type-safe ORM with a powerful query DSL.
reqwest — ergonomic async HTTP client; the go-to for calling external APIs.
tower-http — production middleware: tracing, CORS, compression, auth.
jsonwebtoken — JWT creation and verification.
validator — struct-level input validation with derive macros.