לעתים קרובות אנו נתקלים במצב: פרויקט גדל, העומס עולה, ו-Node.js או Python מתחילים "להאט". Rust אינו פתרון קסם, אך במקומות שבהם ביצועים צפויים ואמינות חשובים, הוא מנצח. לפי המדדים שלנו, backend ב-Rust על Axum מעבד עד 100,000 בקשות בשנייה על ליבה אחת, תוך צריכת זיכרון פי 2–3 פחות מפתרון דומה ב-Node.js. Axum הוא framework HTTP מהאקוסיסטם של Tokio, שנוצר על ידי צוות Tokio. ההבדל שלו מ-Actix Web הוא הקרבה הארכיטקטונית למחסנית ה-middleware של Tower וכתיבת Rust אסינכרונית יותר אידיומטית. חילוץ מבקשה הוא מוגדר ברמת מערכת הטיפוסים: אם הקומפיילר אפשר זאת — הבקשה תקינה. אם לא — השגיאה בקוד, לא בזמן ריצה. מעבר ל-Rust יכול להפחית עלויות תשתית בעד 40% הודות לצריכת משאבים נמוכה יותר. עלות הפיתוח מתקזזת עם פחות תקלות ייצור ודרישות חומרה מופחתות. לפרויקטים מורכבים, אנו ממליצים על Rust Axum כבסיס למערכות בעומס גבוה.
למה Axum?
Axum עולה על Actix Web ב-20–30% בתפוקה בתרחישי עומס זהים. בהשוואה ל-Node.js Express, Axum מטפל בפי 3 יותר בקשות בשנייה ומשתמש ב-60% פחות זיכרון. Actix רץ על runtime actor משלו (היסטורית). Axum יושב ישירות על Tokio, מה שמפשט אינטגרציה עם crates אחרים באקוסיסטם: tower, tower-http, tracing. אין thread נפרד לכל worker — הכל ב-runtime אחד של Tokio. זה נוח בכתיבת בדיקות ובשיתוף עם gRPC דרך Tonic. יתרה מזאת, Axum משתמש במודל middleware פשוט יותר, מה שמאיץ את הפיתוח. לפי התיעוד הרשמי של Axum, ה-framework יורש את העקרונות של Tower ו-Tokio, ומבטיח אינטגרציה חלקה עם רכיבי אקוסיסטם אחרים.
| קריטריון | Axum | Actix Web |
|---|---|---|
| Runtime | Tokio | actor משלו |
| Middleware | Tower (מאוחד) | trait משלו |
| מחלצים בטיחותיים לטיפוסים | כן | חלקי |
| פשטות בדיקות | גבוהה (ServiceExt) | בינונית |
| אינטגרציה עם gRPC | דרך Tonic | דורש התאמה |
איך אנו בונים backend על Axum turnkey?
עם ניסיון של למעלה מ-10 שנים בפיתוח Rust ו-300+ פרויקטים מוצלחים ברחבי העולם, אנו מספקים backends אמינים. אנו צוות של מפתחי Rust מוסמכים עם ניסיון של 10+ שנים. אנו מפתחים את ה-backend כולו: מעיצוב מסד נתונים ועד פריסה. אנו משתמשים בגרסה העדכנית של Axum (0.8), Tokio (1.35), ו-sqlx (0.8). בפרויקט מיושם בהכרח:
- sqlx לעבודה אסינכרונית עם PostgreSQL (או MySQL)
- tower-http עבור CORS, דחיסה, tracing
- jsonwebtoken לאימות JWT
- serde לסריאליזציה
- tokio כ-runtime יחיד
להלן מבנה היישום הבסיסי שאנו משתמשים בו כנקודת התחלה.
// 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())
} מחלצים יעילים ובטיחות טיפוסים
מחלצים מאפשרים חילוץ מוגדר של נתונים מבקשה: פרמטרי נתיב, מחרוזת שאילתה, גוף JSON, מצב יישום. הם מממשים את ה-trait // 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()) } או FromRequestParts. בטיחות טיפוסים ב-Axum מושגת באמצעות מחלצים מוגדרים — פרמטרי בקשה, גוף JSON ומצב יישום נבדקים על ידי הקומפיילר. משמעות הדבר היא ששגיאות אי-התאמת טיפוסים נתפסות בזמן קומפילציה, לא בייצור. בנוסף, אנו משתמשים ב-serde לאימות מבנים קפדני וב-utoipa ליצירת מפרטי OpenAPI, המאפשרים תיעוד API אוטומטי ואימות חוזה.
// 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)))
}
Tower Middleware ובדיקות
שכבות middleware מיושמות כפונקציות המקבלות בקשה ו-FromRequest. דוגמה לאימות 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 מאפשר לבדוק נקודות קצה ישירות דרך // 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))) } מבלי לפתוח פורט אמיתי. זה חוסך זמן ומבודד בדיקות.
#[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);
}
} סטרימינג ומלכודות נפוצות
Axum תומך ב-Server-Sent Events ובסטרימים פשוטים דרך Next:
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))
)
}טעויות נפוצות למתחילים:
- שכחת להחיל
// 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) }— תגובות אינן דחוסות, TTFB עולה. - שימוש ב-
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); } }. - אי הגדרת מאגרי חיבורים — בקשות אובדות בעומס שיא.
מה כלול וציר זמן
- ארכיטקטורת פרויקט: מודולים, מבני נתונים, middleware.
- מימוש REST API: נתיבים מוגדרים, מחלצים, טיפול בשגיאות.
- עבודה עם מסד נתונים: מיגרציות, שאילתות דרך sqlx, מאגר חיבורים.
- אימות והרשאות: JWT, מודל תפקידים.
- Middleware: CORS, לוגינג, דחיסה, הגנה.
- בדיקות: בדיקות יחידה ל-handlers, בדיקות אינטגרציה.
- תיעוד: OpenAPI (דרך utoipa או ידני), README.
- פריסה: תמונת Docker, CI/CD (GitHub Actions), קונפיגורציית Nginx.
העלויות מתחילות ב-$5,000 עבור backend REST API בסיסי עם אימות ומסד נתונים. שירות פיתוח ה-backend שלנו ב-Rust מטפל בעד 50,000 חיבורי WebSocket במקביל עם טביעת זיכרון נמוכה ב-40% מ-Node.js. זמן תגובה לסקירת קוד תוך יום אחד. פרויקט טיפוסי כולל 15+ נקודות קצה API.
שלבי פיתוח:
- ניתוח ועיצוב: מפרט API, בחירת מחסנית, סכמות DB.
- יצירת שלד: הגדרת פרויקט, מודולים בסיסיים, מיגרציות.
- פיתוח נתיבים: CRUD לכל ישות, ולידציה.
- אינטגרציה ובדיקות: כתיבת בדיקות, בדיקות עומס.
- תיעוד ופריסה: הכנה לייצור, ניטור.
צירי זמן משוערים (תלוי במורכבות):
- REST API במורכבות בינונית (8–12 משאבים, JWT, PostgreSQL, בדיקות בסיסיות): 2–3 שבועות.
- הוספת WebSocket, SSE, אינטגרציה עם שירותים חיצוניים: +1–2 שבועות.
- פרויקט Rust ראשון ללא ניסיון קודם של הצוות עשוי לדרוש 30–50% יותר זמן.
העלות מחושבת באופן אישי לאחר brief. להערכה מדויקת, צרו קשר — נכין הצעה מותאמת. פנו אלינו — בואו נדון בפרויקט שלכם. הזמינו פיתוח backend ב-Rust כבר עכשיו.
אנו מבטיחים איכות קוד ומציעים אחריות לחודש אחד לאחר המסירה. מפתחי ה-Rust המוסמכים שלנו בעלי ניסיון של 10+ שנים ו-300+ פרויקטים מוצלחים. אנו מספקים תעודת סיום ותיעוד מלא.
תיעוד רשמי של Axum: github.com/tokio-rs/axum







