Разработка бэкенда сайта на Rust (Actix Web)
Actix Web — один из самых быстрых HTTP-фреймворков в существующих бенчмарках. На TechEmpower benchmark он стабильно в первой пятёрке среди всех языков и фреймворков. Платой за производительность является более высокий порог входа: ownership, lifetimes, async Rust — это не то, что осваивается за выходные.
Когда выбирают Actix Web
Сервисы с жёсткими требованиями к latency (< 1ms p99), обработка финансовых транзакций, высоконагруженные API-шлюзы, инфраструктурные компоненты — это территория Rust. Также: когда нужна предсказуемость потребления памяти без GC-пауз, или когда сервис работает в embedded/edge-среде с ограниченными ресурсами.
Структура приложения
// main.rs
use actix_web::{middleware, web, App, HttpServer};
use sqlx::PgPool;
mod config;
mod db;
mod errors;
mod handlers;
mod models;
mod services;
#[actix_web::main]
async fn main() -> std::io::Result<()> {
dotenvy::dotenv().ok();
tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.init();
let cfg = config::Config::from_env().expect("invalid config");
let pool = PgPool::connect(&cfg.database_url).await.expect("db connect failed");
sqlx::migrate!("./migrations").run(&pool).await.expect("migration failed");
let pool = web::Data::new(pool);
HttpServer::new(move || {
App::new()
.app_data(pool.clone())
.app_data(web::JsonConfig::default().error_handler(errors::json_error_handler))
.wrap(middleware::Logger::default())
.wrap(middleware::Compress::default())
.service(
web::scope("/api/v1")
.service(handlers::users::scope())
.service(handlers::orders::scope()),
)
})
.bind(("0.0.0.0", cfg.port))?
.workers(num_cpus::get())
.run()
.await
}
Модели и запросы к БД через sqlx
sqlx проверяет SQL-запросы во время компиляции — опечатки и несоответствие типов становятся ошибками сборки, а не рантайм-паниками:
// models/user.rs
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use time::OffsetDateTime;
use uuid::Uuid;
#[derive(Debug, Serialize, FromRow)]
pub struct User {
pub id: Uuid,
pub email: String,
pub display_name: String,
#[serde(skip)]
pub password_hash: String,
pub created_at: OffsetDateTime,
}
#[derive(Debug, Deserialize)]
pub struct CreateUserPayload {
pub email: String,
pub display_name: String,
pub password: String,
}
// db/users.rs
pub async fn find_by_id(pool: &PgPool, id: Uuid) -> sqlx::Result<Option<User>> {
sqlx::query_as!(
User,
r#"
SELECT id, email, display_name, password_hash, created_at
FROM users
WHERE id = $1
"#,
id
)
.fetch_optional(pool)
.await
}
pub async fn create(pool: &PgPool, payload: &CreateUserPayload) -> sqlx::Result<User> {
let hash = bcrypt::hash(&payload.password, bcrypt::DEFAULT_COST).unwrap();
sqlx::query_as!(
User,
r#"
INSERT INTO users (id, email, display_name, password_hash)
VALUES ($1, $2, $3, $4)
RETURNING *
"#,
Uuid::new_v4(),
payload.email,
payload.display_name,
hash
)
.fetch_one(pool)
.await
}
Обработчики и роутинг
// handlers/users.rs
use actix_web::{get, post, web, HttpResponse, Scope};
use sqlx::PgPool;
use uuid::Uuid;
use crate::{db, errors::AppError, models::user::CreateUserPayload};
pub fn scope() -> Scope {
web::scope("/users")
.service(get_user)
.service(create_user)
}
#[get("/{id}")]
async fn get_user(
pool: web::Data<PgPool>,
id: web::Path<Uuid>,
) -> Result<HttpResponse, AppError> {
let user = db::users::find_by_id(&pool, *id)
.await?
.ok_or(AppError::NotFound("user not found".into()))?;
Ok(HttpResponse::Ok().json(user))
}
#[post("")]
async fn create_user(
pool: web::Data<PgPool>,
payload: web::Json<CreateUserPayload>,
) -> Result<HttpResponse, AppError> {
let user = db::users::create(&pool, &payload).await?;
Ok(HttpResponse::Created().json(user))
}
Обработка ошибок
// errors.rs
use actix_web::{HttpResponse, ResponseError};
use serde_json::json;
#[derive(Debug, thiserror::Error)]
pub enum AppError {
#[error("not found: {0}")]
NotFound(String),
#[error("validation error: {0}")]
Validation(String),
#[error("database error")]
Database(#[from] sqlx::Error),
#[error("unauthorized")]
Unauthorized,
}
impl ResponseError for AppError {
fn error_response(&self) -> HttpResponse {
match self {
AppError::NotFound(msg) => HttpResponse::NotFound().json(json!({ "error": msg })),
AppError::Validation(msg) => {
HttpResponse::UnprocessableEntity().json(json!({ "error": msg }))
}
AppError::Unauthorized => HttpResponse::Unauthorized().json(json!({ "error": "unauthorized" })),
AppError::Database(e) => {
tracing::error!("db error: {:?}", e);
HttpResponse::InternalServerError().json(json!({ "error": "internal error" }))
}
}
}
}
Middleware для аутентификации
// middleware/auth.rs
use actix_web::{dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, Error};
use futures_util::future::{ok, LocalBoxFuture, Ready};
use jsonwebtoken::{decode, DecodingKey, Validation};
pub struct JwtAuth;
impl<S, B> Transform<S, ServiceRequest> for JwtAuth
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
// ... стандартная реализация Transform
fn new_transform(&self, service: S) -> Self::Future {
ok(JwtAuthMiddleware { service })
}
}
Cargo.toml зависимости
[dependencies]
actix-web = "4"
sqlx = { version = "0.8", features = ["postgres", "uuid", "time", "runtime-tokio"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
uuid = { version = "1", features = ["v4", "serde"] }
time = { version = "0.3", features = ["serde"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
jsonwebtoken = "9"
bcrypt = "0.15"
thiserror = "1"
dotenvy = "0.15"
num_cpus = "1"
Деплой
Итоговый бинарник — 5–15 МБ, без рантайма. Минимальный Docker-образ:
FROM rust:1.77-slim AS builder
WORKDIR /app
COPY . .
RUN cargo build --release
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/target/release/myapi /usr/local/bin/
CMD ["myapi"]
Или scratch-образ если нет динамических зависимостей:
FROM scratch
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/myapi /
CMD ["/myapi"]
Сроки
Actix Web требует больше времени на разработку, чем Rails или Node.js. Простой CRUD API (5–8 ресурсов): 2–3 недели с учётом настройки инфраструктуры и тестов. Высоконагруженный сервис с кастомными middleware, connection pool tuning и нагрузочным тестированием: 4–7 недель. Время разработки компенсируется операционными расходами: один инстанс Actix заменяет 5–10 Node.js-сервисов под аналогичной нагрузкой.







