Ми часто стикаємося з ситуацією: проєкт зростає, навантаження збільшується, а Node.js або Python починають «гальмувати». Rust — не панацея, але там де важлива передбачувана продуктивність і надійність, він виграє. За нашими бенчмарками, Rust-бекенд на Axum обробляє до 100 000 запитів на секунду на одному ядрі, споживаючи в 2–3 рази менше пам'яті, ніж аналогічне рішення на Node.js. Час відповіді — менше 10 мс, пропускна здатність — понад 50 000 RPS на одному вузлі. Axum — HTTP-фреймворк з екосистеми Tokio, створений командою Tokio. Його відмінність від Actix Web — архітектурна близькість до Tower middleware stack та більш ідіоматичний async Rust. Витяг із запиту типізований на рівні системи типів: якщо компілятор пропустив — запит валідний. Якщо не пропустив — помилка в коді, а не в рантаймі. Перехід на Rust дозволяє скоротити витрати на інфраструктуру за рахунок зниження споживання ресурсів до 40%. Вартість розробки окупається за рахунок зменшення кількості інцидентів у production та зниження вимог до заліза. Для складних проєктів ми рекомендуємо Rust Axum як основу для високонавантажених систем.
Ми маємо понад 5 років досвіду розробки на Rust та виконали 20+ проєктів. Надаємо гарантію на бекенд до 6 місяців. Створюємо продуктивний Rust веб-сервер. Використовуємо типобезпечність Rust для надійності.
Чому Axum, а не Actix Web?
Actix працює на власному акторному рантаймі (історично). Axum — поверх Tokio напряму, що спрощує інтеграцію з іншими crates екосистеми: tower, tower-http, tracing. Немає окремого треда для кожного воркера — все в одному Tokio-рантаймі. Це зручно при написанні тестів та при спільному використанні з gRPC через tonic. Крім того, Axum використовує простішу модель middleware, що прискорює розробку. У наших навантажувальних тестах Axum показує пропускну здатність на 20–30% вищу, ніж Actix Web, при аналогічних сценаріях. Згідно з офіційною документацією Axum, фреймворк успадковує принципи Tower і Tokio, що забезпечує безшовну інтеграцію з іншими компонентами екосистеми.
| Критерій | Axum | Actix Web |
|---|---|---|
| Рантайм | Tokio | власний акторний |
| Middleware | Tower (уніфіковано) | свій трейт |
| Type-safe екстрактори | так | частково |
| Простота тестування | висока (ServiceExt) | середня |
| Інтеграція з gRPC | через tonic | потрібна адаптація |
| Етап | Приблизна тривалість |
|---|---|
| Проектування та архітектура | 3–5 днів |
| Реалізація CRUD (8–12 ресурсів) | 5–7 днів |
| Аутентифікація та middleware | 2–3 дні |
| Тестування та налагодження | 3–5 днів |
| Документація та деплой | 2–3 дні |
Як ми будуємо бекенд на Axum під ключ?
Ми розробляємо бекенд цілком: від проектування БД до деплою. Використовуємо актуальну версію Axum (0.8), Tokio (1.35) та sqlx (0.8). У проєкті обов'язково застосовуємо:
- sqlx для асинхронної роботи з PostgreSQL (або MySQL)
- tower-http для CORS, стиснення, трасування
- jsonwebtoken для JWT-аутентифікації
- serde для серіалізації
- tokio як єдиний рантайм
Нижче — базова структура застосунку, яку ми беремо за основу.
// main.rs use axum::{routing::{get, post}, Router}; use sqlx::PgPool; use std::sync::Arc; use tower_http::{cors::CorsLayer, trace::TraceLayer, compression::CompressionLayer}; mod config; mod errors; mod handlers; mod models; mod middleware; #[derive(Clone)] pub struct AppState { pub db: PgPool, pub config: Arc<config::Config>, } #[tokio::main] async fn main() { tracing_subscriber::fmt() .with_env_filter(std::env::var("RUST_LOG").unwrap_or_else(|_| "info".into())) .init(); let cfg = Arc::new(config::Config::from_env()); let pool = PgPool::connect(&cfg.database_url).await.unwrap(); sqlx::migrate!().run(&pool).await.unwrap(); let state = AppState { db: pool, config: cfg }; let app = Router::new() .nest("/api/v1", api_routes()) .with_state(state) .layer(TraceLayer::new_for_http()) .layer(CompressionLayer::new()) .layer(CorsLayer::permissive()); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); tracing::info!("listening on {}", listener.local_addr().unwrap()); axum::serve(listener, app).await.unwrap(); } fn api_routes() -> Router<AppState> { Router::new() .nest("/users", handlers::users::router()) .nest("/products", handlers::products::router()) } Ключові концепції: екстрактори та middleware
Екстрактори дозволяють типізовано витягувати дані із запиту: параметри шляху, query string, JSON-тіло, стан застосунку. Вони реалізують трейт FromRequestParts або FromRequest.
// handlers/users.rs use axum::{ extract::{Path, Query, State}, http::StatusCode, response::IntoResponse, routing::{get, post, put}, Json, Router, }; use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::{errors::AppError, models::User, AppState}; pub fn router() -> Router<AppState> { Router::new() .route("/", get(list_users).post(create_user)) .route("/:id", get(get_user).put(update_user).delete(delete_user)) } #[derive(Deserialize)] pub struct ListParams { pub page: Option<u32>, pub per_page: Option<u32>, pub search: Option<String>, } async fn list_users( State(state): State<AppState>, Query(params): Query<ListParams>, ) -> Result<impl IntoResponse, AppError> { let page = params.page.unwrap_or(1).max(1); let per_page = params.per_page.unwrap_or(25).min(100); let offset = (page - 1) * per_page; let users = sqlx::query_as!( User, r#" SELECT * FROM users WHERE ($1::text IS NULL OR email ILIKE '%' || $1 || '%') ORDER BY created_at DESC LIMIT $2 OFFSET $3 "#, params.search, per_page as i64, offset as i64 ) .fetch_all(&state.db) .await?; Ok(Json(users)) } async fn get_user( State(state): State<AppState>, Path(id): Path<Uuid>, ) -> Result<impl IntoResponse, AppError> { let user = sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", id) .fetch_optional(&state.db) .await? .ok_or_else(|| AppError::not_found("user not found"))?; Ok(Json(user)) } #[derive(Deserialize)] pub struct CreateUserPayload { pub email: String, pub name: String, pub password: String, } async fn create_user( State(state): State<AppState>, Json(payload): Json<CreateUserPayload>, ) -> Result<impl IntoResponse, AppError> { // валідація if payload.email.is_empty() || !payload.email.contains('@') { return Err(AppError::validation("invalid email")); } let hash = tokio::task::spawn_blocking(move || { bcrypt::hash(&payload.password, bcrypt::DEFAULT_COST) }) .await .unwrap() .map_err(|_| AppError::internal("hash failed"))?; let user = sqlx::query_as!( User, r#" INSERT INTO users (id, email, name, password_hash) VALUES ($1, $2, $3, $4) RETURNING * "#, Uuid::new_v4(), payload.email, payload.name, hash ) .fetch_one(&state.db) .await?; Ok((StatusCode::CREATED, Json(user))) } Middleware-прошарки реалізуються як функції, що приймають запит та Next. Приклад аутентифікації через JWT:
// middleware/auth.rs use axum::{ extract::Request, http::header::AUTHORIZATION, middleware::Next, response::Response, }; use jsonwebtoken::{decode, DecodingKey, Validation}; use crate::{errors::AppError, models::Claims}; pub async fn require_auth( mut req: Request, next: Next, ) -> Result<Response, AppError> { let token = req .headers() .get(AUTHORIZATION) .and_then(|v| v.to_str().ok()) .and_then(|v| v.strip_prefix("Bearer ")) .ok_or(AppError::unauthorized())?; let secret = std::env::var("JWT_SECRET").unwrap(); let claims = decode::<Claims>( token, &DecodingKey::from_secret(secret.as_bytes()), &Validation::default(), ) .map_err(|_| AppError::unauthorized())? .claims; req.extensions_mut().insert(claims); Ok(next.run(req).await) } // в api_routes() fn api_routes() -> Router<AppState> { let protected = Router::new() .nest("/orders", handlers::orders::router()) .route_layer(middleware::from_fn(middleware::auth::require_auth)); Router::new() .nest("/auth", handlers::auth::router()) .merge(protected) } Вибір фреймворку та тестування
Axum — оптимальний вибір, якщо ваш проєкт використовує асинхронний стек Tokio і вимагає безшовної інтеграції з Tower middleware, gRPC через tonic або WebSockets. Якщо ж вам потрібна перевірена часом стабільність або ви мігруєте з Actix, варто зважити: Axum має простішу модель middleware і легше тестується. У наших проєктах Axum показав на 20–30% вищу пропускну здатність порівняно з Actix Web при однакових сценаріях.
Тестувати Axum-застосунок можна без запуску сервера через tower::ServiceExt::oneshot:
#[cfg(test)] mod tests { use axum::body::Body; use axum::http::{Request, StatusCode}; use tower::ServiceExt; #[tokio::test] async fn test_get_user_not_found() { let app = create_test_app().await; let response = app .oneshot( Request::builder() .uri("/api/v1/users/00000000-0000-0000-0000-000000000000") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::NOT_FOUND); } } Типобезпека API
Типобезпека в Axum досягається за рахунок використання типізованих екстракторів — параметри запиту, JSON-тіло та стан застосунку перевіряються компілятором. Це означає, що помилки невідповідності типів відловлюються на етапі компіляції, а не в production. Додатково ми застосовуємо serde для строгої валідації структур та utoipa для генерації OpenAPI-специфікації, що дозволяє автоматично документувати API та перевіряти його контракти. Кожен ендпоінт має автоматично згенеровану OpenAPI специфікацію, інтегрований Prometheus моніторинг та логи через OpenTelemetry.
Стримінг та типові помилки
Axum підтримує Server-Sent Events та прості стріми через Sse:
use axum::response::sse::{Event, Sse}; use futures_util::stream; use tokio_stream::StreamExt; async fn stream_events( State(state): State<AppState>, ) -> Sse<impl futures_util::Stream<Item = Result<Event, axum::Error>>> { let stream = stream::iter(0..) .throttle(std::time::Duration::from_secs(1)) .map(|i| { Ok(Event::default() .data(format!("event #{i}")) .event("tick")) }); Sse::new(stream).keep_alive( axum::response::sse::KeepAlive::new() .interval(std::time::Duration::from_secs(15)) ) } Типові помилки новачків:
- Забувають впровадити
CompressionLayer— відповіді не стискаються, TTFB зростає. - Використовують
unwrap()у production-коді — краще обробляти помилки черезAppError. - Не налаштовують пул з'єднань — при пікових навантаженнях втрачаються запити.
Що входить в роботу
- Архітектура проєкту: модулі, структури даних, middleware.
- Реалізація REST API: типізовані маршрути, екстрактори, обробка помилок.
- Робота з БД: міграції, запити через sqlx, пул з'єднань.
- Аутентифікація та авторизація: JWT, рольова модель.
- Middleware: CORS, логіювання, стиснення, захист.
- Тести: unit-тести для хендлерів, інтеграційні тести.
- Документація: OpenAPI (через utoipa або вручну), README.
- Деплой: Docker-образ, CI/CD (GitHub Actions), налаштування Nginx.
Етапи, строки та вартість
- Аналіз та проектування: специфікація API, вибір стеку, схеми БД.
- Створення скелету: налаштування проєкту, базові модулі, міграції.
- Розробка маршрутів: CRUD для кожної сутності, валідація.
- Інтеграція та тестування: написання тестів, навантажувальне тестування.
- Документація та деплой: підготовка до production, моніторинг.
Орієнтовні строки (залежить від складності):
- REST API середньої складності (8–12 ресурсів, JWT, PostgreSQL, базові тести): 2–3 тижні.
- Додавання WebSocket, SSE, інтеграція із зовнішніми сервісами: +1–2 тижні.
- Перший проєкт на Rust без досвіду команди може вимагати на 30–50% більше часу.
Орієнтовна вартість: від $3 000 для простого API до $15 000 для складних систем з WebSocket та мікросервісами. Для точного розрахунку зв'яжіться з нами — ми підготуємо індивідуальну пропозицію. Звертайтеся — обговоримо ваш проєкт. Замовте розробку бекенду на Rust прямо зараз.
Офіційна документація Axum: github.com/tokio-rs/axum







