Website Backend Development with Rust (Actix Web)
Actix Web is one of the fastest HTTP frameworks in existing benchmarks. On TechEmpower, it consistently ranks in the top five among all languages and frameworks. Payment for performance is a higher entry bar: ownership, lifetimes, async Rust — these aren't learned over a weekend.
When Actix Web is chosen
Services with strict latency requirements (< 1ms p99), financial transaction processing, high-load API gateways, infrastructure components — Rust territory. Also: when memory consumption predictability without GC pauses is needed, or when the service runs in embedded/edge environments with limited resources.
Application structure
// 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
}
Models and database queries via sqlx
sqlx checks SQL queries at compile time — typos and type mismatches become build errors, not runtime panics:
// 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 and routing
// 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))
}
Error handling
// 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" }))
}
}
}
}
Authentication 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,
{
// ... standard Transform implementation
fn new_transform(&self, service: S) -> Self::Future {
ok(JwtAuthMiddleware { service })
}
}
Cargo.toml dependencies
[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"
Deployment
Final binary is 5–15 MB, no runtime. Minimal Docker image:
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"]
Or scratch image if no dynamic dependencies:
FROM scratch
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/myapi /
CMD ["/myapi"]
Development timeline
Actix Web requires more development time than Rails or Node.js. Simple CRUD API (5–8 resources): 2–3 weeks with infrastructure setup and tests. High-load service with custom middleware, connection pool tuning and load testing: 4–7 weeks. Development time is offset by operational savings: one Actix instance replaces 5–10 Node.js services under similar load.







