Table of contents
Open Table of contents
Cargo.toml
[dependencies]
actix-web = "4"
actix-rt = "2"
actix-cors = "0.7"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
env_logger = "0.11"
1. 最小 Server
use actix_web::{get, App, HttpServer, Responder};
#[get("/")]
async fn hello() -> impl Responder {
"Hello, World!"
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new().service(hello)
})
.bind(("0.0.0.0", 8080))?
.run()
.await
}
2. 路由
宏方式(推荐)
use actix_web::{get, post, put, delete, web, HttpResponse, Responder};
#[get("/users")]
async fn list_users() -> impl Responder {
HttpResponse::Ok().json(serde_json::json!([]))
}
#[post("/users")]
async fn create_user(body: web::Json<CreateUser>) -> impl Responder {
HttpResponse::Created().json(serde_json::json!({"id": 1}))
}
#[get("/users/{id}")]
async fn get_user(path: web::Path<u32>) -> impl Responder {
let id = path.into_inner();
HttpResponse::Ok().json(serde_json::json!({"id": id}))
}
#[put("/users/{id}")]
async fn update_user(
path: web::Path<u32>,
body: web::Json<UpdateUser>,
) -> impl Responder {
HttpResponse::Ok().json(serde_json::json!({"updated": true}))
}
#[delete("/users/{id}")]
async fn delete_user(path: web::Path<u32>) -> impl Responder {
HttpResponse::NoContent().finish()
}
// 注册
App::new()
.service(list_users)
.service(create_user)
.service(get_user)
.service(update_user)
.service(delete_user)
手动方式
App::new()
.route("/health", web::get().to(|| async { "OK" }))
.route("/users", web::get().to(list_users))
.route("/users", web::post().to(create_user))
分组路由(scope)
use actix_web::web;
App::new()
.service(
web::scope("/api/v1")
.service(list_users)
.service(create_user)
.service(get_user)
)
.service(
web::scope("/admin")
.route("/dashboard", web::get().to(dashboard))
)
// 结果: /api/v1/users, /api/v1/users/{id}, /admin/dashboard
Path 参数
use actix_web::web;
#[get("/users/{id}")]
async fn get_user(path: web::Path<u32>) -> impl Responder {
let id = path.into_inner();
format!("User {}", id)
}
// 多个路径参数
#[get("/users/{user_id}/items/{item_id}")]
async fn get_item(path: web::Path<(u32, u32)>) -> impl Responder {
let (user_id, item_id) = path.into_inner();
format!("User {} Item {}", user_id, item_id)
}
// 解构到结构体
#[derive(Deserialize)]
struct ItemPath {
user_id: u32,
item_id: u32,
}
#[get("/users/{user_id}/items/{item_id}")]
async fn get_item_v2(path: web::Path<ItemPath>) -> impl Responder {
format!("User {} Item {}", path.user_id, path.item_id)
}
Query 参数
#[derive(Deserialize)]
struct Pagination {
page: Option<u32>,
per_page: Option<u32>,
}
#[get("/users")]
async fn list_users(query: web::Query<Pagination>) -> impl Responder {
let page = query.page.unwrap_or(1);
format!("Page {}", page)
}
// GET /users?page=2&per_page=10
JSON Body
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
struct CreateUser {
username: String,
email: String,
}
#[derive(Serialize)]
struct User {
id: u64,
username: String,
email: String,
}
#[post("/users")]
async fn create_user(body: web::Json<CreateUser>) -> impl Responder {
let user = User {
id: 1,
username: body.username.clone(),
email: body.email.clone(),
};
HttpResponse::Created().json(user)
}
use actix_web::HttpRequest;
#[get("/info")]
async fn info(req: HttpRequest) -> impl Responder {
let ua = req
.headers()
.get("user-agent")
.and_then(|v| v.to_str().ok())
.unwrap_or("unknown");
format!("UA: {}", ua)
}
4. 共享状态(web::Data)
use actix_web::web;
use std::sync::Mutex;
struct AppState {
db: DbPool,
counter: Mutex<u32>,
}
#[get("/count")]
async fn get_count(data: web::Data<AppState>) -> impl Responder {
let count = data.counter.lock().unwrap();
format!("Count: {}", count)
}
#[post("/increment")]
async fn increment(data: web::Data<AppState>) -> impl Responder {
let mut count = data.counter.lock().unwrap();
*count += 1;
HttpResponse::Ok().json(serde_json::json!({"count": *count}))
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
// 在闭包外创建,web::Data 内部是 Arc
let state = web::Data::new(AppState {
db: DbPool::new(),
counter: Mutex::new(0),
});
HttpServer::new(move || {
App::new()
.app_data(state.clone()) // clone 的是 Arc,不是数据本身
.service(get_count)
.service(increment)
})
.bind(("0.0.0.0", 8080))?
.run()
.await
}
5. 响应
use actix_web::HttpResponse;
// 纯文本
async fn text() -> impl Responder {
"hello"
}
// JSON
async fn json() -> impl Responder {
HttpResponse::Ok().json(serde_json::json!({"status": "ok"}))
}
// 自定义响应
async fn custom() -> impl Responder {
HttpResponse::Ok()
.insert_header(("X-Custom", "value"))
.content_type("text/plain")
.body("custom body")
}
// 无 body
async fn no_content() -> impl Responder {
HttpResponse::NoContent().finish()
}
6. 错误处理
自定义错误类型
use actix_web::{HttpResponse, ResponseError};
use derive_more::{Display, Error};
#[derive(Debug, Display, Error)]
enum AppError {
#[display("Not found: {}", message)]
NotFound { message: String },
#[display("Bad request: {}", message)]
BadRequest { message: String },
#[display("Internal error")]
Internal,
}
impl ResponseError for AppError {
fn error_response(&self) -> HttpResponse {
match self {
AppError::NotFound { .. } => {
HttpResponse::NotFound().json(serde_json::json!({"error": self.to_string()}))
}
AppError::BadRequest { .. } => {
HttpResponse::BadRequest().json(serde_json::json!({"error": self.to_string()}))
}
AppError::Internal => {
HttpResponse::InternalServerError()
.json(serde_json::json!({"error": "Internal server error"}))
}
}
}
}
#[get("/users/{id}")]
async fn get_user(path: web::Path<u32>) -> Result<HttpResponse, AppError> {
let id = path.into_inner();
if id == 0 {
return Err(AppError::BadRequest { message: "ID cannot be 0".into() });
}
if id > 100 {
return Err(AppError::NotFound { message: format!("User {}", id) });
}
Ok(HttpResponse::Ok().json(serde_json::json!({"id": id, "name": "Alice"})))
}
JSON 提取器错误配置
let json_config = web::JsonConfig::default()
.limit(4096) // body 大小限制 4KB
.error_handler(|err, _req| {
actix_web::error::InternalError::from_response(
err,
HttpResponse::BadRequest().json(serde_json::json!({"error": "Invalid JSON"})),
).into()
});
App::new().app_data(json_config)
7. 中间件
内置中间件
use actix_web::middleware;
use actix_cors::Cors;
App::new()
// 请求日志
.wrap(middleware::Logger::default())
// 响应压缩
.wrap(middleware::Compress::default())
// 默认 header
.wrap(middleware::DefaultHeaders::new().add(("X-Version", "1.0")))
// URL 路径规范化(去除尾部斜杠)
.wrap(middleware::NormalizePath::trim())
// CORS
.wrap(
Cors::default()
.allow_any_origin()
.allow_any_method()
.allow_any_header()
)
自定义中间件(from_fn)
use actix_web::{
dev::ServiceRequest,
body::MessageBody,
middleware::{self, Next},
HttpResponse, Error,
};
async fn auth_middleware(
req: ServiceRequest,
next: Next<impl MessageBody>,
) -> Result<actix_web::dev::ServiceResponse<impl MessageBody>, Error> {
let has_auth = req.headers().get("Authorization").is_some();
if has_auth {
next.call(req).await
} else {
let response = HttpResponse::Unauthorized().body("Missing auth");
Ok(req.into_response(response).map_into_right_body())
}
}
// 应用到特定 scope
App::new()
.service(
web::scope("/api")
.wrap(middleware::from_fn(auth_middleware))
.service(list_users)
)
8. 文件上传(Multipart)
# Cargo.toml
actix-multipart = "0.7"
use actix_multipart::form::{tempfile::TempFile, MultipartForm};
#[derive(MultipartForm)]
struct UploadForm {
#[multipart(limit = "50MB")]
file: TempFile,
}
#[post("/upload")]
async fn upload(MultipartForm(form): MultipartForm<UploadForm>) -> impl Responder {
let size = form.file.size;
let filename = form.file.file_name.unwrap_or_default();
format!("Uploaded {} ({} bytes)", filename, size)
}
9. 静态文件
# Cargo.toml
actix-files = "0.6"
use actix_files::Files;
App::new()
.service(Files::new("/static", "./static").prefer_utf8(true))
10. HttpServer 配置
HttpServer::new(|| App::new().service(hello))
.bind(("0.0.0.0", 8080))?
.workers(4) // worker 线程数(默认 = CPU 核数)
.keep_alive(Duration::from_secs(75))
.shutdown_timeout(30) // 优雅关闭超时(秒)
.run()
.await
11. 完整示例
use actix_web::{get, post, web, App, HttpServer, HttpResponse, Responder, middleware};
use actix_cors::Cors;
use serde::{Deserialize, Serialize};
use std::sync::Mutex;
struct AppState {
db: DbPool,
}
#[derive(Deserialize)]
struct Pagination {
page: Option<u32>,
limit: Option<u32>,
}
#[derive(Serialize)]
struct Player {
id: u32,
name: String,
level: u32,
}
#[derive(Deserialize)]
struct CreatePlayer {
name: String,
}
#[get("/players")]
async fn list_players(
data: web::Data<AppState>,
query: web::Query<Pagination>,
) -> impl Responder {
HttpResponse::Ok().json(Vec::<Player>::new())
}
#[get("/players/{id}")]
async fn get_player(
data: web::Data<AppState>,
path: web::Path<u32>,
) -> impl Responder {
let id = path.into_inner();
HttpResponse::Ok().json(Player { id, name: "Bear".into(), level: 15 })
}
#[post("/players")]
async fn create_player(
data: web::Data<AppState>,
body: web::Json<CreatePlayer>,
) -> impl Responder {
let player = Player { id: 1, name: body.name.clone(), level: 1 };
HttpResponse::Created().json(player)
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
env_logger::init();
let state = web::Data::new(AppState { db: DbPool::new() });
HttpServer::new(move || {
App::new()
.app_data(state.clone())
.wrap(middleware::Logger::default())
.wrap(Cors::default().allow_any_origin().allow_any_method().allow_any_header())
.service(list_players)
.service(get_player)
.service(create_player)
})
.bind(("0.0.0.0", 8080))?
.run()
.await
}
12. 与 axum 对比
| actix-web | axum |
|---|
| 路由风格 | 宏 #[get("/path")] | 函数式 route("/path", get(handler)) |
| 状态 | web::Data<T>(内部 Arc) | State<T>(需自己 Clone) |
| 中间件 | 自有 trait + from_fn | tower 生态(通用) |
| 错误处理 | ResponseError trait | IntoResponse trait |
| Worker 模型 | 多 worker 线程,每个有独立 App 实例 | 单 App 实例,靠 tokio 调度 |
| 配置 | HttpServer 链式配置(workers/keep_alive) | 外部 TcpListener + axum::serve |
| 成熟度 | 更老,API 更稳定 | 更新,tokio 官方出品 |