Rust Web Routing
Building APIs with Actix and Axum
Explanation
Web Development in Rust
Rust's web ecosystem has matured significantly. Actix-web and Axum are production-ready frameworks that leverage Rust's safety guarantees while delivering excellent performance.
Key Concepts
- async/await: Rust's async runtime (Tokio)
- Extractors: Type-safe request parsing
- Middleware: Request/response pipeline
- State: Shared application data
Framework Comparison
| Feature | Actix-web | Axum | |---------|-----------|------| | Maturity | Older | Newer | | Approach | Actor model | Tower-based | | Performance | Excellent | Excellent | | Learning | Moderate | Easier |
Demonstration
Example 1: Axum Basics
use axum::{
routing::{get, post, put, delete},
extract::{Path, Query, State, Json},
response::{IntoResponse, Json as JsonResponse},
http::StatusCode,
Router,
};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::RwLock;
// Models
#[derive(Clone, Serialize, Deserialize)]
struct User {
id: u64,
name: String,
email: String,
}
#[derive(Deserialize)]
struct CreateUser {
name: String,
email: String,
}
#[derive(Deserialize)]
struct QueryParams {
page: Option<u32>,
per_page: Option<u32>,
}
// App state
type AppState = Arc<RwLock<Vec<User>>>;
// Handlers
async fn list_users(
State(state): State<AppState>,
Query(params): Query<QueryParams>,
) -> impl IntoResponse {
let users = state.read().await;
let page = params.page.unwrap_or(1);
let per_page = params.per_page.unwrap_or(10);
let start = ((page - 1) * per_page) as usize;
let end = (start + per_page as usize).min(users.len());
let paginated: Vec<_> = users[start..end].to_vec();
JsonResponse(serde_json::json!({
"data": paginated,
"page": page,
"per_page": per_page,
"total": users.len()
}))
}
async fn get_user(
State(state): State<AppState>,
Path(id): Path<u64>,
) -> Result<impl IntoResponse, StatusCode> {
let users = state.read().await;
users
.iter()
.find(|u| u.id == id)
.cloned()
.map(|user| JsonResponse(serde_json::json!({ "data": user })))
.ok_or(StatusCode::NOT_FOUND)
}
async fn create_user(
State(state): State<AppState>,
Json(payload): Json<CreateUser>,
) -> impl IntoResponse {
let mut users = state.write().await;
let id = users.len() as u64 + 1;
let user = User {
id,
name: payload.name,
email: payload.email,
};
users.push(user.clone());
(StatusCode::CREATED, JsonResponse(serde_json::json!({ "data": user })))
}
async fn update_user(
State(state): State<AppState>,
Path(id): Path<u64>,
Json(payload): Json<CreateUser>,
) -> Result<impl IntoResponse, StatusCode> {
let mut users = state.write().await;
if let Some(user) = users.iter_mut().find(|u| u.id == id) {
user.name = payload.name;
user.email = payload.email;
Ok(JsonResponse(serde_json::json!({ "data": user.clone() })))
} else {
Err(StatusCode::NOT_FOUND)
}
}
async fn delete_user(
State(state): State<AppState>,
Path(id): Path<u64>,
) -> StatusCode {
let mut users = state.write().await;
if let Some(pos) = users.iter().position(|u| u.id == id) {
users.remove(pos);
StatusCode::NO_CONTENT
} else {
StatusCode::NOT_FOUND
}
}
#[tokio::main]
async fn main() {
let state: AppState = Arc::new(RwLock::new(vec![]));
let app = Router::new()
.route("/users", get(list_users).post(create_user))
.route("/users/:id", get(get_user).put(update_user).delete(delete_user))
.with_state(state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
println!("Server running on http://localhost:3000");
axum::serve(listener, app).await.unwrap();
}
Example 2: Actix-web Implementation
use actix_web::{web, App, HttpServer, HttpResponse, Result};
use serde::{Deserialize, Serialize};
use std::sync::Mutex;
#[derive(Clone, Serialize, Deserialize)]
struct User {
id: u64,
name: String,
email: String,
}
#[derive(Deserialize)]
struct CreateUser {
name: String,
email: String,
}
struct AppState {
users: Mutex<Vec<User>>,
}
async fn list_users(data: web::Data<AppState>) -> HttpResponse {
let users = data.users.lock().unwrap();
HttpResponse::Ok().json(&*users)
}
async fn get_user(
data: web::Data<AppState>,
path: web::Path<u64>,
) -> HttpResponse {
let id = path.into_inner();
let users = data.users.lock().unwrap();
match users.iter().find(|u| u.id == id) {
Some(user) => HttpResponse::Ok().json(user),
None => HttpResponse::NotFound().json(serde_json::json!({"error": "Not found"})),
}
}
async fn create_user(
data: web::Data<AppState>,
body: web::Json<CreateUser>,
) -> HttpResponse {
let mut users = data.users.lock().unwrap();
let user = User {
id: users.len() as u64 + 1,
name: body.name.clone(),
email: body.email.clone(),
};
users.push(user.clone());
HttpResponse::Created().json(user)
}
async fn update_user(
data: web::Data<AppState>,
path: web::Path<u64>,
body: web::Json<CreateUser>,
) -> HttpResponse {
let id = path.into_inner();
let mut users = data.users.lock().unwrap();
if let Some(user) = users.iter_mut().find(|u| u.id == id) {
user.name = body.name.clone();
user.email = body.email.clone();
HttpResponse::Ok().json(user.clone())
} else {
HttpResponse::NotFound().json(serde_json::json!({"error": "Not found"}))
}
}
async fn delete_user(
data: web::Data<AppState>,
path: web::Path<u64>,
) -> HttpResponse {
let id = path.into_inner();
let mut users = data.users.lock().unwrap();
if let Some(pos) = users.iter().position(|u| u.id == id) {
users.remove(pos);
HttpResponse::NoContent().finish()
} else {
HttpResponse::NotFound().json(serde_json::json!({"error": "Not found"}))
}
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let state = web::Data::new(AppState {
users: Mutex::new(vec![]),
});
HttpServer::new(move || {
App::new()
.app_data(state.clone())
.route("/users", web::get().to(list_users))
.route("/users", web::post().to(create_user))
.route("/users/{id}", web::get().to(get_user))
.route("/users/{id}", web::put().to(update_user))
.route("/users/{id}", web::delete().to(delete_user))
})
.bind("127.0.0.1:8080")?
.run()
.await
}
Example 3: Middleware and Error Handling
use axum::{
extract::Request,
http::{StatusCode, header},
middleware::{self, Next},
response::{IntoResponse, Response},
Json, Router,
};
use serde_json::json;
// Custom error type
#[derive(Debug)]
enum ApiError {
NotFound(String),
BadRequest(String),
Unauthorized,
InternalError(String),
}
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
let (status, message) = match self {
ApiError::NotFound(msg) => (StatusCode::NOT_FOUND, msg),
ApiError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg),
ApiError::Unauthorized => (StatusCode::UNAUTHORIZED, "Unauthorized".to_string()),
ApiError::InternalError(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg),
};
(status, Json(json!({ "error": message }))).into_response()
}
}
// Logging middleware
async fn logging_middleware(request: Request, next: Next) -> Response {
let method = request.method().clone();
let uri = request.uri().clone();
let start = std::time::Instant::now();
let response = next.run(request).await;
let duration = start.elapsed();
println!(
"{} {} - {:?} - {}ms",
method,
uri,
response.status(),
duration.as_millis()
);
response
}
// Auth middleware
async fn auth_middleware(request: Request, next: Next) -> Result<Response, ApiError> {
let auth_header = request
.headers()
.get(header::AUTHORIZATION)
.and_then(|h| h.to_str().ok());
match auth_header {
Some(token) if token.starts_with("Bearer ") => {
// Validate token...
Ok(next.run(request).await)
}
_ => Err(ApiError::Unauthorized),
}
}
// CORS middleware
async fn cors_middleware(request: Request, next: Next) -> Response {
let mut response = next.run(request).await;
response.headers_mut().insert(
header::ACCESS_CONTROL_ALLOW_ORIGIN,
"*".parse().unwrap(),
);
response.headers_mut().insert(
header::ACCESS_CONTROL_ALLOW_METHODS,
"GET, POST, PUT, DELETE, OPTIONS".parse().unwrap(),
);
response
}
// Using middleware
fn create_router() -> Router {
let public_routes = Router::new()
.route("/health", axum::routing::get(|| async { "OK" }))
.route("/login", axum::routing::post(login_handler));
let protected_routes = Router::new()
.route("/users", axum::routing::get(list_users))
.layer(middleware::from_fn(auth_middleware));
Router::new()
.merge(public_routes)
.merge(protected_routes)
.layer(middleware::from_fn(logging_middleware))
.layer(middleware::from_fn(cors_middleware))
}
async fn login_handler() -> impl IntoResponse {
Json(json!({ "token": "your-jwt-token" }))
}
async fn list_users() -> impl IntoResponse {
Json(json!({ "users": [] }))
}
Key Takeaways:
- Axum and Actix-web are production-ready
- Extractors provide type-safe request parsing
- Use Arc
for shared mutable state - Custom error types improve API consistency
- Middleware handles cross-cutting concerns
Imitation
Challenge 1: Add Request Validation
Task: Create a validation extractor for request bodies.
Solution
use axum::{
async_trait,
extract::{FromRequest, Request},
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
use serde::de::DeserializeOwned;
use validator::Validate;
struct ValidatedJson<T>(pub T);
#[async_trait]
impl<S, T> FromRequest<S> for ValidatedJson<T>
where
T: DeserializeOwned + Validate,
S: Send + Sync,
{
type Rejection = Response;
async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
let Json(value) = Json::<T>::from_request(req, state)
.await
.map_err(|e| {
(StatusCode::BAD_REQUEST, Json(serde_json::json!({
"error": "Invalid JSON"
}))).into_response()
})?;
value.validate().map_err(|e| {
(StatusCode::BAD_REQUEST, Json(serde_json::json!({
"error": "Validation failed",
"details": e.to_string()
}))).into_response()
})?;
Ok(ValidatedJson(value))
}
}
// Usage
#[derive(Deserialize, Validate)]
struct CreateUser {
#[validate(length(min = 2))]
name: String,
#[validate(email)]
email: String,
}
async fn create_user(ValidatedJson(payload): ValidatedJson<CreateUser>) -> impl IntoResponse {
// Payload is already validated
Json(serde_json::json!({ "name": payload.name }))
}
Practice
Exercise 1: Build a Todo API
Difficulty: Intermediate
Create a complete Todo API with:
- CRUD operations
- User authentication
- Database persistence (SQLx)
Exercise 2: WebSocket Chat
Difficulty: Advanced
Implement a chat server:
- WebSocket connections
- Room-based messaging
- Connection management
Summary
What you learned:
- Axum and Actix-web basics
- Type-safe extractors
- Error handling patterns
- Middleware implementation
- State management
Next Steps:
- Read: Rust OOP
- Practice: Build a REST API
- Explore: SQLx for databases
