Skip to main content
โšก Calmops

Rust for Web Development: Complete Guide to Web Services

Introduction

Rust is becoming increasingly popular for web development due to its performance and safety. This guide covers building web services with Rust, from basic setup to production deployment.

Why Rust for Web?

Feature Rust Go Node.js
Performance โšกโšกโšก โšกโšก โšก
Safety Memory safe GC Manual
Concurrency Fearless Great Event loop
Learning Curve Steep Easy Easy
Ecosystem Growing Mature Huge

Web Frameworks

Axum

Modern, ergonomic web framework:

use axum::{
    routing::{get, post},
    Router,
};
use serde::{Deserialize, Serialize};

#[derive(Deserialize, Serialize)]
struct User {
    name: String,
    email: String,
}

async fn hello() -> &'static str {
    "Hello, World!"
}

async fn create_user(Json(user): Json<User>) -> Json<User> {
    // Save to database...
    Json(user)
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/", get(hello))
        .route("/users", post(create_user));
    
    axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

Actix-web

High-performance, actor-based:

use actix_web::{web, App, HttpResponse, HttpServer, Responder};

async fn hello() -> impl Responder {
    HttpResponse::Ok().body("Hello, World!")
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .route("/", web::get().to(hello))
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

Database Integration

With SQLx

use sqlx::{postgres::PgPoolOptions, Row};

#[derive(Debug)]
struct User {
    id: i64,
    name: String,
    email: String,
}

async fn get_users(pool: &PgPool) -> Result<Vec<User>, sqlx::Error> {
    let rows = sqlx::query("SELECT id, name, email FROM users")
        .fetch_all(pool)
        .await?;
    
    Ok(rows
        .iter()
        .map(|row| User {
            id: row.get("id"),
            name: row.get("name"),
            email: row.get("email"),
        })
        .collect())
}

With Diesel

use diesel::prelude::*;
use models::User;

fn list_users(conn: &mut PgConnection) -> QueryResult<Vec<User>> {
    users::table.load::<User>(conn)
}

Error Handling

use axum::{
    extract::Path,
    response::Json,
    http::StatusCode,
};

async fn get_user(Path(id): Path<i64>) -> Result<Json<User>, AppError> {
    let user = db::find_user(id).await?;
    
    match user {
        Some(u) => Ok(Json(u)),
        None => Err(AppError::NotFound("User not found".into())),
    }
}

enum AppError {
    NotFound(String),
    Database(String),
}

impl From<sqlx::Error> for AppError {
    fn from(err: sqlx::Error) -> Self {
        AppError::Database(err.to_string())
    }
}

Authentication

JWT Auth

use jsonwebtoken::{encode, decode, Header, Validation};
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
struct Claims {
    sub: String,
    exp: usize,
}

fn create_token(user_id: &str) -> String {
    let expiration = Utc::now() + Duration::hours(24);
    
    let claims = Claims {
        sub: user_id.to_string(),
        exp: expiration.timestamp() as usize,
    };
    
    encode(&Header::default(), &claims, "secret".as_ref()).unwrap()
}

fn validate_token(token: &str) -> Result<Claims, jsonwebtoken::errors::Error> {
    let secret = "secret".as_ref();
    decode(token, secret, &Validation::default())
        .map(|data| data.claims)
}

State Management

use std::sync::Arc;
use tokio::sync::Mutex;

struct AppState {
    db: Pool,
    cache: Arc<Mutex<LruCache<String, String>>>,
}

async fn handler(
    State(state): State<Arc<AppState>>,
) -> impl IntoResponse {
    let mut cache = state.cache.lock().await;
    // Use cache...
}

Deployment

Docker

# Build stage
FROM rust:1.75 as builder
WORKDIR /app
COPY . .
RUN cargo build --release

# Runtime stage
FROM debian:bookworm-slim
COPY --from=builder /app/target/release/my-app /usr/local/bin/
CMD ["my-app"]

Production Checklist

// 1. Use release profile
// Cargo.toml
[profile.release]
lto = true
codegen-units = 1
opt-level = 3

// 2. Add logging
 tracing::info!("Request processed");

// 3. Add metrics
 metrics::counter!("requests_total").increment(1);

// 4. Graceful shutdown
use tokio::signal;
use tokio::sync::oneshot;

let (tx, rx) = oneshot::channel::<()>();

tokio::spawn(async {
    signal::ctrl_c().await.unwrap();
    tx.send(()).unwrap();
});

let _ = rx.await;

Conclusion

Rust for web is excellent when you need:

  • Maximum performance
  • Memory safety guarantees
  • Low-level control
  • WebAssembly targets

Start with Axum for simplicity, use Actix-web for performance.


External Resources

Comments