Why Rust for Web APIs?
Rust web APIs consistently top benchmarks — Actix-web handles millions of requests per second on modest hardware, with memory usage measured in megabytes rather than gigabytes. The ownership model eliminates entire classes of bugs (null pointer dereferences, data races, use-after-free) at compile time.
The cost: Rust has a steep learning curve. The benefits justify it for performance-critical services, but for most CRUD APIs, a language like Go or Python offers a better productivity/performance tradeoff.
Setting Up an Actix-web Project
# Create a new Rust project
cargo new my-api
cd my-api
# Cargo.toml
[dependencies]
actix-web = "4"
serde = {{ version = "1", features = ["derive"] }}
serde_json = "1"
tokio = {{ version = "1", features = ["full"] }}
sqlx = {{ version = "0.7", features = ["postgres", "runtime-tokio-native-tls", "uuid", "chrono"] }}
uuid = {{ version = "1", features = ["v4", "serde"] }}
chrono = {{ version = "0.4", features = ["serde"] }}
dotenv = "0.15"
Basic Routing and Handlers
use actix_web::{{web, App, HttpServer, HttpResponse, Result}};
use serde::{{Deserialize, Serialize}};
#[derive(Serialize, Deserialize)]
struct User {{
id: u64,
name: String,
email: String,
}}
async fn get_users() -> Result<HttpResponse> {{
let users = vec![
User {{ id: 1, name: "Alice".into(), email: "[email protected]".into() }},
];
Ok(HttpResponse::Ok().json(users))
}}
async fn create_user(user: web::Json<User>) -> Result<HttpResponse> {{
// In a real app, save to database here
Ok(HttpResponse::Created().json(user.into_inner()))
}}
#[actix_web::main]
async fn main() -> std::io::Result<()> {{
HttpServer::new(|| {{
App::new()
.route("/users", web::get().to(get_users))
.route("/users", web::post().to(create_user))
}})
.bind("127.0.0.1:8080")?
.run()
.await
}}
Database Integration with SQLx
use sqlx::PgPool;
use uuid::Uuid;
#[derive(sqlx::FromRow, Serialize)]
struct Post {{
id: Uuid,
title: String,
body: String,
user_id: Uuid,
}}
async fn get_posts(pool: web::Data<PgPool>) -> Result<HttpResponse> {{
let posts = sqlx::query_as!(
Post,
"SELECT id, title, body, user_id FROM posts ORDER BY created_at DESC LIMIT 20"
)
.fetch_all(pool.get_ref())
.await
.map_err(|e| actix_web::error::ErrorInternalServerError(e))?;
Ok(HttpResponse::Ok().json(posts))
}}
// Initialize pool and share via App::app_data
let pool = PgPool::connect(&database_url).await?;
App::new().app_data(web::Data::new(pool))
Error Handling
use actix_web::{{HttpResponse, ResponseError}};
use std::fmt;
#[derive(Debug)]
enum ApiError {{
NotFound(String),
DatabaseError(sqlx::Error),
ValidationError(String),
}}
impl fmt::Display for ApiError {{
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {{
match self {{
ApiError::NotFound(msg) => write!(f, "Not found: {{}}", msg),
ApiError::DatabaseError(e) => write!(f, "Database error: {{}}", e),
ApiError::ValidationError(msg) => write!(f, "Validation error: {{}}", msg),
}}
}}
}}
impl ResponseError for ApiError {{
fn error_response(&self) -> HttpResponse {{
match self {{
ApiError::NotFound(msg) => HttpResponse::NotFound().json(json!({{"error": msg}})),
ApiError::DatabaseError(_) => HttpResponse::InternalServerError()
.json(json!({{"error": "Internal server error"}})),
ApiError::ValidationError(msg) => HttpResponse::BadRequest()
.json(json!({{"error": msg}})),
}}
}}
}}
Frequently Asked Questions
How does Actix-web compare to Axum?
Both are production-ready. Axum (from the Tokio team) uses a more ergonomic extractor system and integrates more naturally with Tower middleware. Actix-web has a larger ecosystem and is more mature. For new projects, Axum is often recommended for its cleaner API design.
Can I use Rust for serverless functions?
Yes. AWS Lambda supports Rust via the lambda_runtime crate. Cloudflare Workers supports Rust via WebAssembly with the worker crate. Cold start times are dramatically better than Node.js or Python.
What's the recommended way to structure a larger Rust API?
Organize by domain: src/routes/, src/models/, src/services/, src/errors.rs. Use Actix's scope routing to group related endpoints. Keep handlers thin — business logic belongs in service structs, not route handlers.