Actix Web Cheat Sheet
Overview
Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust. Built on top of the Actix actor system and Tokio async runtime, it consistently ranks at the top of web framework benchmarks. Actix Web uses Rust’s type system to provide compile-time guarantees about request handling, making it virtually impossible to have runtime type errors in route handlers.
The framework provides type-safe request extractors, middleware support, WebSocket handling, and HTTP/2 support out of the box. Its handler functions use async/await syntax and the extractor pattern, where request data is automatically deserialized and validated based on function parameter types. Actix Web is used in production by companies requiring high throughput and reliability.
Installation
Setup
# Create new project
cargo new my-api
cd my-api
Cargo.toml
[package]
name = "my-api"
version = "0.1.0"
edition = "2021"
[dependencies]
actix-web = "4"
actix-rt = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }
env_logger = "0.11"
log = "0.4"
Minimal Server
use actix_web::{web, App, HttpServer, HttpResponse, middleware};
#[actix_web::main]
async fn main() -> std::io::Result<()> {
env_logger::init();
HttpServer::new(|| {
App::new()
.wrap(middleware::Logger::default())
.route("/", web::get().to(|| async {
HttpResponse::Ok().json(serde_json::json!({"message": "Hello, World!"}))
}))
})
.bind("0.0.0.0:8080")?
.run()
.await
}
Routing
Route Registration
use actix_web::{web, App, HttpResponse};
fn config(cfg: &mut web::ServiceConfig) {
cfg.service(
web::scope("/api")
.route("/users", web::get().to(get_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))
);
}
// Using macro attributes
#[actix_web::get("/health")]
async fn health() -> HttpResponse {
HttpResponse::Ok().json(serde_json::json!({"status": "ok"}))
}
#[actix_web::post("/users")]
async fn create_user(body: web::Json<CreateUser>) -> HttpResponse {
HttpResponse::Created().json(body.into_inner())
}
// Register in App
App::new()
.configure(config)
.service(health)
.service(create_user)
Path Parameters
use actix_web::{web, HttpResponse};
// Single parameter
#[actix_web::get("/users/{id}")]
async fn get_user(path: web::Path<String>) -> HttpResponse {
let id = path.into_inner();
HttpResponse::Ok().json(serde_json::json!({"id": id}))
}
// Multiple parameters
#[actix_web::get("/users/{user_id}/posts/{post_id}")]
async fn get_post(path: web::Path<(String, String)>) -> HttpResponse {
let (user_id, post_id) = path.into_inner();
HttpResponse::Ok().json(serde_json::json!({
"user_id": user_id,
"post_id": post_id
}))
}
// Typed path extraction
#[derive(serde::Deserialize)]
struct PostPath {
user_id: u64,
post_id: u64,
}
#[actix_web::get("/users/{user_id}/posts/{post_id}")]
async fn get_post_typed(path: web::Path<PostPath>) -> HttpResponse {
HttpResponse::Ok().json(serde_json::json!({
"user_id": path.user_id,
"post_id": path.post_id
}))
}
Scopes and Guards
use actix_web::{web, guard, App};
App::new()
.service(
web::scope("/api/v1")
.guard(guard::Header("Accept", "application/json"))
.route("/users", web::get().to(get_users))
)
.service(
web::scope("/admin")
.wrap(auth_middleware)
.route("/dashboard", web::get().to(dashboard))
)
.default_service(web::route().to(not_found))
Extractors
Common Extractors
| Extractor | Source | Example |
|---|---|---|
web::Path<T> | URL path parameters | web::Path<(String, u32)> |
web::Query<T> | Query string | web::Query<SearchParams> |
web::Json<T> | JSON request body | web::Json<CreateUser> |
web::Form<T> | Form data | web::Form<LoginForm> |
web::Data<T> | Application state | web::Data<AppState> |
HttpRequest | Full request | Direct access |
Query Parameters
#[derive(serde::Deserialize)]
struct SearchParams {
q: String,
#[serde(default = "default_page")]
page: u32,
#[serde(default = "default_limit")]
limit: u32,
}
fn default_page() -> u32 { 1 }
fn default_limit() -> u32 { 20 }
#[actix_web::get("/search")]
async fn search(query: web::Query<SearchParams>) -> HttpResponse {
HttpResponse::Ok().json(serde_json::json!({
"query": query.q,
"page": query.page,
"limit": query.limit
}))
}
JSON Body
#[derive(serde::Deserialize, serde::Serialize)]
struct CreateUser {
name: String,
email: String,
#[serde(default)]
age: Option<u32>,
}
#[actix_web::post("/users")]
async fn create_user(
body: web::Json<CreateUser>,
db: web::Data<DbPool>,
) -> actix_web::Result<HttpResponse> {
let user = body.into_inner();
let result = db.insert_user(&user).await
.map_err(actix_web::error::ErrorInternalServerError)?;
Ok(HttpResponse::Created().json(result))
}
Application State
struct AppState {
db: DbPool,
config: AppConfig,
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let state = web::Data::new(AppState {
db: create_pool().await,
config: load_config(),
});
HttpServer::new(move || {
App::new()
.app_data(state.clone())
.service(get_users)
})
.bind("0.0.0.0:8080")?
.run()
.await
}
#[actix_web::get("/users")]
async fn get_users(state: web::Data<AppState>) -> HttpResponse {
let users = state.db.get_users().await;
HttpResponse::Ok().json(users)
}
Middleware
Built-in Middleware
use actix_web::middleware;
App::new()
.wrap(middleware::Logger::default())
.wrap(middleware::Compress::default())
.wrap(middleware::NormalizePath::trim())
.wrap(middleware::DefaultHeaders::new()
.add(("X-Version", "1.0"))
.add(("X-Content-Type-Options", "nosniff")))
Custom Middleware
use actix_web::{dev::{Service, ServiceRequest, ServiceResponse, Transform}, Error};
use std::future::{Future, Ready, ready};
use std::pin::Pin;
use std::time::Instant;
pub struct Timer;
impl<S, B> Transform<S, ServiceRequest> for Timer
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Transform = TimerMiddleware<S>;
type InitError = ();
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ready(Ok(TimerMiddleware { service }))
}
}
pub struct TimerMiddleware<S> {
service: S,
}
impl<S, B> Service<ServiceRequest> for TimerMiddleware<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
fn poll_ready(&self, cx: &mut std::task::Context<'_>) -> std::task::Poll<Result<(), Self::Error>> {
self.service.poll_ready(cx)
}
fn call(&self, req: ServiceRequest) -> Self::Future {
let start = Instant::now();
let fut = self.service.call(req);
Box::pin(async move {
let mut res = fut.await?;
let elapsed = start.elapsed();
res.headers_mut().insert(
actix_web::http::header::HeaderName::from_static("x-response-time"),
actix_web::http::header::HeaderValue::from_str(
&format!("{}ms", elapsed.as_millis())
).unwrap(),
);
Ok(res)
})
}
}
Configuration
Server Configuration
HttpServer::new(|| {
App::new()
.service(index)
})
.bind("0.0.0.0:8080")?
.workers(4) // Number of worker threads
.max_connections(25_000) // Max concurrent connections
.max_connection_rate(256) // Max connection rate
.keep_alive(std::time::Duration::from_secs(75))
.client_request_timeout(std::time::Duration::from_secs(5))
.shutdown_timeout(30) // Graceful shutdown timeout
.run()
.await
JSON Configuration
// Custom JSON config
let json_cfg = web::JsonConfig::default()
.limit(4096) // Max payload size
.error_handler(|err, _req| {
let detail = err.to_string();
actix_web::error::InternalError::from_response(
err,
HttpResponse::BadRequest().json(serde_json::json!({
"error": "Invalid JSON",
"detail": detail
})),
).into()
});
App::new()
.app_data(json_cfg)
Advanced Usage
Error Handling
use actix_web::{HttpResponse, ResponseError};
use std::fmt;
#[derive(Debug)]
enum AppError {
NotFound(String),
BadRequest(String),
Internal(String),
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
AppError::NotFound(msg) => write!(f, "Not found: {}", msg),
AppError::BadRequest(msg) => write!(f, "Bad request: {}", msg),
AppError::Internal(msg) => write!(f, "Internal error: {}", msg),
}
}
}
impl ResponseError for AppError {
fn error_response(&self) -> HttpResponse {
match self {
AppError::NotFound(msg) =>
HttpResponse::NotFound().json(serde_json::json!({"error": msg})),
AppError::BadRequest(msg) =>
HttpResponse::BadRequest().json(serde_json::json!({"error": msg})),
AppError::Internal(msg) =>
HttpResponse::InternalServerError().json(serde_json::json!({"error": msg})),
}
}
}
WebSocket
use actix_web::{web, HttpRequest, HttpResponse};
use actix_web_actors::ws;
struct MyWs;
impl actix::Actor for MyWs {
type Context = ws::WebsocketContext<Self>;
}
impl actix::StreamHandler<Result<ws::Message, ws::ProtocolError>> for MyWs {
fn handle(&mut self, msg: Result<ws::Message, ws::ProtocolError>, ctx: &mut Self::Context) {
match msg {
Ok(ws::Message::Text(text)) => ctx.text(text),
Ok(ws::Message::Ping(msg)) => ctx.pong(&msg),
Ok(ws::Message::Close(reason)) => ctx.close(reason),
_ => (),
}
}
}
#[actix_web::get("/ws")]
async fn websocket(req: HttpRequest, stream: web::Payload) -> HttpResponse {
ws::start(MyWs {}, &req, stream).unwrap()
}
Testing
#[cfg(test)]
mod tests {
use super::*;
use actix_web::{test, App};
#[actix_web::test]
async fn test_health() {
let app = test::init_service(
App::new().service(health)
).await;
let req = test::TestRequest::get().uri("/health").to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), 200);
let body: serde_json::Value = test::read_body_json(resp).await;
assert_eq!(body["status"], "ok");
}
#[actix_web::test]
async fn test_create_user() {
let app = test::init_service(
App::new().service(create_user)
).await;
let user = serde_json::json!({"name": "Alice", "email": "a@b.com"});
let req = test::TestRequest::post()
.uri("/users")
.set_json(&user)
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), 201);
}
}
Troubleshooting
| Problem | Solution |
|---|---|
App data not configured | Ensure app_data() called before routes; check Data<T> type matches |
| Extractor type mismatch | Verify struct derives Deserialize; check JSON field names |
| Borrow checker errors | Use web::Data<T> for shared state; clone when needed |
| Connection refused | Check bind address; ensure port is available |
| Slow compilation | Use cargo-watch for dev; split into workspace |
| Memory usage high | Check for unbounded channels; tune worker count |
PayloadError | Increase JsonConfig limit; check Content-Type header |
| Middleware order | Middleware wraps in reverse order of .wrap() calls |