Третья статья серии про Rust в backend. В предыдущей разбирали async-runtime и Tokio — фундамент, на котором стоят все веб-фреймворки Rust. Теперь — этаж выше: чем писать HTTP-сервис. Сравним два основных фреймворка, Axum и Actix-web, на одном и том же REST API, разберём роль Tower и дадим матрицу выбора.
В статье
- Кто есть кто в экосистеме
- Axum: фреймворк поверх Tower
- Actix-web: зрелость и скорость
- Один REST API на двух фреймворках
- Middleware и error handling
- Tower как фундамент
- Что выбирать под задачу
Кто есть кто в экосистеме
Веб-фреймворков на Rust много, но в production-разговоре их два:
- Axum — от команды Tokio, построен на
towerиhyper. Типобезопасный роутинг, экстракторы, прямая интеграция с экосистемой Tower. Сегодня — де-факто выбор сообщества по умолчанию. - Actix-web — один из самых зрелых и быстрых, со своей системой middleware и экстракторов, исторический лидер бенчмарков.
Остальные занимают ниши: Warp (тоже от Tokio, на «фильтрах» — мощно, но эргономика на любителя, импульс спал), Poem (моложе, из коробки OpenAPI), Rocket (очень эргономичный, теперь async). Для нового сервиса в 9 случаях из 10 выбор стоит между Axum и Actix-web — на них и сосредоточимся.
Axum: фреймворк поверх Tower
Идея Axum — минимум своего, максимум переиспользования. Это, по сути, удобный роутер поверх hyper, где каждый сервис — это Tower Service, а middleware — Tower Layer. Handler’ы получают данные через экстракторы (Json, State, Path, Query), а возвращают всё, что реализует IntoResponse.
use axum::{routing::post, Router, Json, extract::State, http::StatusCode};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
#[derive(Clone)]
struct AppState { /* db: sqlx::PgPool, ... */ }
#[derive(Deserialize)]
struct CreateUser { name: String }
#[derive(Serialize)]
struct User { id: i64, name: String }
async fn create_user(
State(_state): State<Arc<AppState>>,
Json(payload): Json<CreateUser>,
) -> Result<(StatusCode, Json<User>), StatusCode> {
if payload.name.is_empty() {
return Err(StatusCode::BAD_REQUEST);
}
Ok((StatusCode::CREATED, Json(User { id: 1, name: payload.name })))
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/users", post(create_user))
.with_state(Arc::new(AppState {}));
let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await.unwrap();
axum::serve(listener, app)
.with_graceful_shutdown(async { tokio::signal::ctrl_c().await.ok(); })
.await
.unwrap();
}Сильная сторона — типобезопасность: несоответствие экстрактора и handler’а не скомпилируется. И graceful shutdown здесь штатный — with_graceful_shutdown.
Actix-web: зрелость и скорость
Actix-web старше, очень обкатан в production и стабильно держится в топе TechEmpower. У него своя система роутинга (в т.ч. через атрибуты-макросы), свои экстракторы и middleware. Тот же эндпоинт:
use actix_web::{web, App, HttpServer, HttpResponse, post};
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
struct CreateUser { name: String }
#[derive(Serialize)]
struct User { id: i64, name: String }
#[post("/users")]
async fn create_user(payload: web::Json<CreateUser>) -> HttpResponse {
if payload.name.is_empty() {
return HttpResponse::BadRequest().finish();
}
HttpResponse::Created().json(User { id: 1, name: payload.name.clone() })
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| App::new().service(create_user))
.bind(("0.0.0.0", 8080))?
.run()
.await
}Про производительность важно сказать честно: на синтетике Actix-web обычно чуть впереди, Axum — рядом. Но реальный сервис почти всегда упирается в базу, сеть и сериализацию, а не во фреймворк — на практике разница между ними теряется на фоне запроса к PostgreSQL. Выбирать по строчке в TechEmpower не стоит.
Один REST API на двух фреймворках
Концептуально код одинаков: разобрать JSON → провалидировать → сходить в БД → отдать ответ. Различия — в деталях API:
| Axum | Actix-web | |
|---|---|---|
| Роутинг | Router::new().route(...) |
App::service(...) / макросы #[get] |
| JSON-вход | экстрактор Json<T> |
web::Json<T> |
| Состояние | State<T> (с with_state) |
web::Data<T> (app_data) |
| Ответ | любой IntoResponse |
HttpResponse / impl Responder |
| БД | sqlx/sea-orm (любой async) |
то же |
Слой БД для обоих одинаков — async-драйвер вроде sqlx (запросы с проверкой типов на этапе компиляции) или ORM sea-orm. Фреймворк здесь не диктует ничего: пул соединений кладётся в состояние и достаётся экстрактором.
Оба варианта — один и тот же POST /users с валидацией, middleware и обработкой ошибок — собраны и покрыты тестами в digital-cookbook, rust/web-frameworks/: крейты axum-api и actix-api, cargo test гоняет оба handler’а без поднятия порта.
Middleware и error handling
Middleware. В Axum это Tower Layer — и сразу доступна готовая экосистема tower-http: логирование/трейсинг (TraceLayer), CORS, сжатие, таймауты, лимиты тела:
use tower_http::{trace::TraceLayer, cors::CorsLayer};
let app = Router::new()
.route("/users", post(create_user))
.layer(TraceLayer::new_for_http())
.layer(CorsLayer::permissive())
.with_state(state);В Actix-web — свой механизм .wrap(...) и свои крейты (actix-web::middleware::Logger, actix-cors). Тоже зрело и полно, просто не Tower.
Error handling. В Axum ошибка — это тип, реализующий IntoResponse: handler возвращает Result<T, AppError>, а AppError сам решает, в какой HTTP-ответ превратиться. В Actix-web аналогично через трейт ResponseError. В обоих случаях идиома одна: доменная ошибка → её отображение в HTTP, без россыпи unwrap().
Tower как фундамент
Понимать Tower стоит, даже если берёте Actix. Tower — это абстракция «асинхронного сервиса»: трейт Service отображает запрос в future ответа, а Layer оборачивает один сервис в другой. Звучит абстрактно, но даёт конкретное: middleware (таймауты, ретраи, rate-limiting, трейсинг, load-balancing) пишется один раз и переиспользуется в любом Tower-сервисе — не только в вебе.
Поэтому выбор Axum — это не только про HTTP: тот же стек Tower лежит под gRPC-фреймворком tonic, под клиентами и фоновыми сервисами. Один набор middleware на весь сервис, а не отдельный для каждого слоя. Actix-web живёт в своей экосистеме — прекрасной, но отдельной.
Что выбирать под задачу
Оба фреймворка production-ready; «неправильного» выбора тут нет. Ориентир:
- Axum — если хотите Tokio-нативность, экосистему Tower (общие middleware для HTTP, gRPC, фоновых задач), современную эргономику и максимальный импульс сообщества. Разумный выбор по умолчанию для нового сервиса.
- Actix-web — если нужна выжатая до предела пропускная способность на синтетике, у вас уже есть actix-кодовая база, либо команда привыкла к его модели.
- Для типового REST/CRUD берите тот, что приятнее команде: на реальной нагрузке (БД, сеть) разница между ними практически не видна.
Практическое правило: начинаете с нуля и нет сильного аргумента в пользу Actix — берите Axum, чтобы переиспользовать Tower на всём сервисе.
Дальше в серии — минимальные Docker-образы для Rust: как упаковать такой сервис в образ на 5–15 MB.
Комментарии