Introduction
Rust has emerged as a top choice for building high-performance web services. Known for its memory safety and zero-cost abstractions, Rust enables developers to create web applications that are both fast and safe. In 2026, the Rust web ecosystem has matured significantly, with production-ready frameworks and excellent tooling.
This guide covers Rust web development in 2026, from choosing a framework to deploying production services. Whether you’re building APIs or full-stack applications, this guide provides practical insights for modern Rust web development.
The Rust Web Ecosystem
Why Rust for Web?
Rust offers compelling advantages for web development:
- Performance: Near C-level speed, ideal for high-throughput services
- Memory Safety: No null pointers, no buffer overflows
- Concurrency: Fearless parallelism with async/await
- Tooling: Excellent package manager (cargo), great IDE support
Key Frameworks
| Framework | Focus | Performance | Maturity |
|---|---|---|---|
| Axum | Ergonomics | Excellent | High |
| Actix-web | Performance | Best | High |
| Rocket | Developer Experience | Good | High |
| Warp | Composable | Excellent | Medium |
Axum Framework
Getting Started
use axum::{
routing::{get, post},
Router,
};
use serde::{Deserialize, Serialize};
use std::net::SocketAddr;
#[derive(Debug, Serialize, Deserialize)]
struct User {
id: i32,
name: String,
email: String,
}
async fn get_users() -> Vec<User> {
vec![
User { id: 1, name: "Alice".into(), email: "[email protected]".into() },
User { id: 2, name: "Bob".into(), email: "[email protected]".into() },
]
}
async fn create_user(Json(user): Json<CreateUserRequest>) -> Json<User> {
// Create user in database
Json(User {
id: 3,
name: user.name,
email: user.email,
})
}
#[derive(Debug, Deserialize)]
struct CreateUserRequest {
name: String,
email: String,
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/users", get(get_users).post(create_user))
.route("/health", get(|| async { "OK" }));
let addr = SocketAddr::from(([0, 0, 0, 0], 3000));
println!("Server running on {}", addr);
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
axum::serve(listener, app).await.unwrap();
}
Middleware
use axum::{
extract::Request,
middleware::Next,
response::Response,
Router,
};
use std::time::Instant;
async fn timing_middleware(
request: Request,
next: Next,
) -> Response {
let start = Instant::now();
let response = next.run(request).await;
let duration = start.elapsed();
println!("Request took {}ms", duration.as_millis());
response
}
let app = Router::new()
.route("/api/users", get(get_users))
.layer(axum::middleware::from_fn(timing_middleware));
State Management
use axum::{
extract::State,
Router,
};
use std::sync::Arc;
use tokio::sync::RwLock;
use std::collections::HashMap;
#[derive(Clone)]
struct AppState {
users: Arc<RwLock<HashMap<i32, User>>>,
db: Arc<DbPool>,
}
async fn get_user(
State(state): State<AppState>,
Path(user_id): Path<i32>,
) -> Result<Json<User>, AppError> {
let users = state.users.read().await;
users.get(&user_id)
.cloned()
.map(Json)
.ok_or(AppError::NotFound)
}
let state = AppState {
users: Arc::new(RwLock::new(HashMap::new())),
db: pool,
};
let app = Router::new()
.route("/users/:id", get(get_user))
.with_state(state);
Actix-web
High-Performance Server
use actix_web::{web, App, HttpResponse, HttpServer, Responder};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
struct Item {
id: i32,
name: String,
}
async fn get_items() -> impl Responder {
web::block(|| {
// Blocking database call in thread pool
vec![
Item { id: 1, name: "Item 1".into() },
Item { id: 2, name: "Item 2".into() },
]
})
.await
.map(|items| HttpResponse::Ok().json(items))
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.route("/items", web::get().to(get_items))
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
WebSocket Support
use actix_web::{web, App, HttpServer, HttpResponse, Error};
use actix_ws::{Session, Message};
use std::sync::Arc;
use tokio::sync::broadcast;
async fn ws_handler(
req: actix_web::HttpRequest,
stream: actix_web::web::Payload,
) -> Result<HttpResponse, Error> {
let (response, session, msg_stream) = actix_ws::handle(&req, stream)?;
tokio::spawn(async move {
let mut session = session;
while let Some(msg) = msg_stream.next().await {
match msg {
Ok(Message::Text(text)) => {
session.text(text).await?;
}
Ok(Message::Close(_)) => break,
_ => {}
}
}
session.close(None).await.ok();
});
Ok(response)
}
Database Integration
SQLx
use sqlx::{postgres::PgPoolOptions, Row, PgRow};
#[derive(Debug)]
struct User {
id: i32,
name: String,
email: String,
}
impl From<PgRow> for User {
fn from(row: PgRow) -> Self {
User {
id: row.get("id"),
name: row.get("name"),
email: row.get("email"),
}
}
}
async fn get_users(pool: &PgPool) -> Result<Vec<User>, sqlx::Error> {
let users = sqlx::query_as!(
User,
"SELECT id, name, email FROM users WHERE active = true"
)
.fetch_all(pool)
.await?;
Ok(users)
}
// With parameters
async fn get_user_by_id(pool: &PgPool, id: i32) -> Result<Option<User>, sqlx::Error> {
let user = sqlx::query_as!(
User,
"SELECT id, name, email FROM users WHERE id = $1",
id
)
.fetch_optional(pool)
.await?;
Ok(user)
}
SeaORM
use sea_orm::{EntityTrait, QueryFilter, ActiveModel};
#[derive(Clone)]
struct DbConnection {
conn: sea_orm::DbConn,
}
impl DbConnection {
async fn create_user(&self, name: String, email: String) -> Result<User, sea_orm::DbErr> {
let active_model = user::ActiveModel {
name: Set(name),
email: Set(email),
..Default::default()
};
User::insert(active_model)
.exec(&self.conn)
.await
.map(|insert_result| {
User {
id: insert_result.last_insert_id,
name,
email,
}
})
}
async fn find_user(&self, id: i32) -> Result<Option<User>, sea_orm::DbErr> {
User::find_by_id(id)
.one(&self.conn)
.await
}
}
Authentication
JWT Auth
use jsonwebtoken::{encode, decode, Header, Validation, EncodingKey, DecodingKey};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
struct Claims {
sub: String,
exp: usize,
}
const SECRET: &[u8] = b"your_secret_key";
fn create_token(user_id: &str) -> Result<String, jsonwebtoken::errors::Error> {
let expiration = chrono::Utc::now()
.checked_add_signed(chrono::Duration::hours(24))
.unwrap()
.timestamp();
let claims = Claims {
sub: user_id.to_string(),
exp: expiration,
};
encode(&Header::default(), &claims, &EncodingKey::from_secret(SECRET))
}
fn validate_token(token: &str) -> Result<Claims, jsonwebtoken::errors::Error> {
let token_data = decode::<Claims>(
token,
&DecodingKey::from_secret(SECRET),
&Validation::default(),
)?;
Ok(token_data.claims)
}
// Axum middleware
async fn auth_middleware(
headers: HeaderMap,
next: Next,
) -> Response {
match headers.get("Authorization") {
Some(auth_header) => {
let token = auth_header.to_str().unwrap_or("");
match validate_token(token) {
Ok(claims) => {
// Add user ID to extensions
let mut req = request.into_request();
req.extensions_mut().insert(claims.sub);
next.run(req).await
}
Err(_) => HttpResponse::Unauthorized().into(),
}
}
None => HttpResponse::Unauthorized().into(),
}
}
Error Handling
use axum::{
response::{IntoResponse, Response},
Json,
};
use serde_json::json;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum AppError {
#[error("User not found")]
NotFound,
#[error("Invalid input: {0}")]
BadRequest(String),
#[error("Database error: {0}")]
DatabaseError(String),
#[error("Unauthorized")]
Unauthorized,
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let (status, error_message) = match self {
AppError::NotFound => (StatusCode::NOT_FOUND, "Not found"),
AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, &msg),
AppError::DatabaseError(msg) => (StatusCode::INTERNAL_SERVER_ERROR, &msg),
AppError::Unauthorized => (StatusCode::UNAUTHORIZED, "Unauthorized"),
};
let body = Json(json!({
"error": error_message,
}));
(status, body).into_response()
}
}
Testing
Unit Tests
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_user_request() {
let request = CreateUserRequest {
name: "Alice".into(),
email: "[email protected]".into(),
};
assert_eq!(request.name, "Alice");
assert!(request.email.contains("@"));
}
#[test]
fn test_token_generation() {
let token = create_token("user123").unwrap();
assert!(!token.is_empty());
let claims = validate_token(&token).unwrap();
assert_eq!(claims.sub, "user123");
}
}
Integration Tests
#[cfg(test)]
mod integration_tests {
use super::*;
use axum::{
body::Body,
routing::get,
Router,
};
#[tokio::test]
async fn test_health_endpoint() {
let app = Router::new()
.route("/health", get(|| async { "OK" }));
let response = app
.oneshot(
Request::builder()
.uri("/health")
.body(Body::empty())
.unwrap()
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
}
Deployment
Docker
# Build stage
FROM rust:1.75 as builder
WORKDIR /app
COPY . .
RUN cargo build --release
# Runtime stage
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y \
libssl3 \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY --from=builder /app/target/release/myapp .
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["./myapp"]
Kubernetes
apiVersion: apps/v1
kind: Deployment
metadata:
name: rust-api
spec:
replicas: 3
selector:
matchLabels:
app: rust-api
template:
metadata:
labels:
app: rust-api
spec:
containers:
- name: api
image: myapp:latest
ports:
- containerPort: 3000
resources:
limits:
memory: "512Mi"
cpu: "500m"
requests:
memory: "256Mi"
cpu: "250m"
External Resources
Documentation
Learning
- Rust by Example - Learn Rust
- The Book - Official Rust book
- Rust for Rustaceans - Advanced Rust
Community
- Rust Discord - Community
- r/rust - Reddit
- This Week in Rust - Newsletter
Conclusion
Rust web development in 2026 offers excellent frameworks, great tooling, and outstanding performance. Whether you choose Axum for its ergonomics or Actix-web for maximum speed, you’ll build reliable, high-performance services.
Start with Axum if you want productivity. Choose Actix-web if raw performance is critical. Either way, embrace Rust’s safety guarantees and build robust web services.
The ecosystem continues to grow. Stay engaged with the community and keep learning.
Comments