Chapter 8: Real-World Projects

Time to put it all together! This chapter walks through building production-ready applications using the intermediate concepts you've learned.

8.1 Project 1: High-Performance REST API

Architecture Overview

// Project structure
project/
├── Cargo.toml
├── src/
│   ├── main.rs
│   ├── api/
│   │   ├── mod.rs
│   │   ├── handlers.rs
│   │   └── middleware.rs
│   ├── db/
│   │   ├── mod.rs
│   │   └── pool.rs
│   ├── models/
│   │   ├── mod.rs
│   │   └── user.rs
│   └── error.rs
└── tests/
    └── integration_test.rs

Error Types with thiserror

// src/error.rs
use thiserror::Error;

#[derive(Error, Debug)]
pub enum ApiError {
    #[error("Database error: {0}")]
    Database(#[from] sqlx::Error),

    #[error("Not found: {0}")]
    NotFound(String),

    #[error("Validation error: {0}")]
    Validation(String),

    #[error("Unauthorized")]
    Unauthorized,
}

impl axum::response::IntoResponse for ApiError {
    fn into_response(self) -> axum::response::Response {
        use axum::http::StatusCode;
        use axum::Json;
        use serde_json::json;

        let (status, message) = match self {
            ApiError::Database(_) => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()),
            ApiError::NotFound(_) => (StatusCode::NOT_FOUND, self.to_string()),
            ApiError::Validation(_) => (StatusCode::BAD_REQUEST, self.to_string()),
            ApiError::Unauthorized => (StatusCode::UNAUTHORIZED, self.to_string()),
        };

        (status, Json(json!({ "error": message }))).into_response()
    }
}

Database Layer with Connection Pool

// src/db/pool.rs
use sqlx::{PgPool, postgres::PgPoolOptions};
use std::time::Duration;

pub async fn create_pool(database_url: &str) -> Result<PgPool, sqlx::Error> {
    PgPoolOptions::new()
        .max_connections(10)
        .acquire_timeout(Duration::from_secs(3))
        .connect(database_url)
        .await
}

// src/models/user.rs
use sqlx::FromRow;
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize, FromRow)]
pub struct User {
    pub id: i64,
    pub name: String,
    pub email: String,
    pub created_at: chrono::DateTime<chrono::Utc>,
}

#[derive(Debug, Deserialize)]
pub struct CreateUser {
    pub name: String,
    pub email: String,
}

pub async fn create_user(pool: &PgPool, user: CreateUser) -> Result<User, sqlx::Error> {
    sqlx::query_as::<_, User>(
        "INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *"
    )
    .bind(&user.name)
    .bind(&user.email)
    .fetch_one(pool)
    .await
}

pub async fn get_user(pool: &PgPool, id: i64) -> Result<User, sqlx::Error> {
    sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = $1")
        .bind(id)
        .fetch_one(pool)
        .await
}

API Handlers with Axum

// src/api/handlers.rs
use axum::{
    extract::{Path, State},
    Json,
};
use std::sync::Arc;

use crate::{error::ApiError, models::*};

pub type AppState = Arc<sqlx::PgPool>;

pub async fn create_user_handler(
    State(pool): State<AppState>,
    Json(payload): Json<CreateUser>,
) -> Result<Json<User>, ApiError> {
    let user = create_user(&pool, payload).await?;
    Ok(Json(user))
}

pub async fn get_user_handler(
    State(pool): State<AppState>,
    Path(id): Path<i64>,
) -> Result<Json<User>, ApiError> {
    let user = get_user(&pool, id)
        .await
        .map_err(|_| ApiError::NotFound(format!("User {} not found", id)))?;
    Ok(Json(user))
}

// src/main.rs
use axum::{
    routing::{get, post},
    Router,
};
use std::sync::Arc;

#[tokio::main]
async fn main() {
    let pool = db::create_pool(&std::env::var("DATABASE_URL").unwrap())
        .await
        .unwrap();

    let app_state = Arc::new(pool);

    let app = Router::new()
        .route("/users", post(api::create_user_handler))
        .route("/users/:id", get(api::get_user_handler))
        .with_state(app_state);

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
        .await
        .unwrap();

    axum::serve(listener, app).await.unwrap();
}

8.2 Project 2: CLI Tool with Rich Features

Project Setup

// Cargo.toml
[dependencies]
clap = { version = "4", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
anyhow = "1"
colored = "2"

// src/cli.rs
use clap::{Parser, Subcommand};

#[derive(Parser)]
#[command(name = "mytool")]
#[command(about = "A modern CLI tool", long_about = None)]
struct Cli {
    #[command(subcommand)]
    command: Commands,

    #[arg(short, long, global = true)]
    verbose: bool,
}

#[derive(Subcommand)]
enum Commands {
    /// Initialize a new project
    Init {
        #[arg(short, long)]
        name: String,

        #[arg(short, long, default_value = ".")]
        path: String,
    },

    /// Build the project
    Build {
        #[arg(short, long)]
        release: bool,
    },

    /// Run tests
    Test {
        #[arg(short, long)]
        coverage: bool,
    },
}

Colorful Output

use colored::*;

fn print_success(msg: &str) {
    println!("{} {}", "✓".green().bold(), msg);
}

fn print_error(msg: &str) {
    eprintln!("{} {}", "✗".red().bold(), msg);
}

fn print_info(msg: &str) {
    println!("{} {}", "ℹ".blue().bold(), msg);
}

async fn run_init(name: String, path: String) -> anyhow::Result<()> {
    print_info(&format!("Initializing project '{}'...", name));

    tokio::fs::create_dir_all(&path).await?;
    print_success(&format!("Created directory: {}", path));

    let config = serde_json::json!({
        "name": name,
        "version": "0.1.0",
    });

    tokio::fs::write(
        format!("{}/config.json", path),
        serde_json::to_string_pretty(&config)?
    ).await?;

    print_success("Project initialized successfully!");
    Ok(())
}

8.3 Project 3: WebSocket Server

use axum::{
    extract::ws::{WebSocket, WebSocketUpgrade},
    response::Response,
    routing::get,
    Router,
};
use std::sync::Arc;
use tokio::sync::broadcast;

type Tx = broadcast::Sender<String>;
type Rx = broadcast::Receiver<String>;

#[derive(Clone)]
struct AppState {
    tx: Tx,
}

async fn ws_handler(
    ws: WebSocketUpgrade,
    axum::extract::State(state): axum::extract::State<AppState>,
) -> Response {
    ws.on_upgrade(|socket| handle_socket(socket, state))
}

async fn handle_socket(socket: WebSocket, state: AppState) {
    let (mut sender, mut receiver) = socket.split();
    let mut rx = state.tx.subscribe();

    // Spawn task to forward broadcast messages to this client
    let mut send_task = tokio::spawn(async move {
        while let Ok(msg) = rx.recv().await {
            if sender.send(msg.into()).await.is_err() {
                break;
            }
        }
    });

    // Receive messages from this client and broadcast
    let tx = state.tx.clone();
    let mut recv_task = tokio::spawn(async move {
        while let Some(Ok(msg)) = receiver.next().await {
            if let axum::extract::ws::Message::Text(text) = msg {
                let _ = tx.send(text);
            }
        }
    });

    // Wait for either task to finish
    tokio::select! {
        _ = (&mut send_task) => recv_task.abort(),
        _ = (&mut recv_task) => send_task.abort(),
    }
}

#[tokio::main]
async fn main() {
    let (tx, _rx) = broadcast::channel(100);

    let state = AppState { tx };

    let app = Router::new()
        .route("/ws", get(ws_handler))
        .with_state(state);

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
        .await
        .unwrap();

    axum::serve(listener, app).await.unwrap();
}

8.4 Project 4: Background Job Processor

use tokio::sync::mpsc;
use serde::{Deserialize, Serialize};
use std::time::Duration;

#[derive(Debug, Serialize, Deserialize)]
enum Job {
    SendEmail { to: String, subject: String, body: String },
    GenerateReport { user_id: u64 },
    ProcessImage { path: String },
}

struct Worker {
    id: usize,
    receiver: mpsc::Receiver<Job>,
}

impl Worker {
    async fn run(mut self) {
        while let Some(job) = self.receiver.recv().await {
            println!("Worker {} processing job: {:?}", self.id, job);

            match job {
                Job::SendEmail { to, subject, body } => {
                    self.send_email(&to, &subject, &body).await;
                }
                Job::GenerateReport { user_id } => {
                    self.generate_report(user_id).await;
                }
                Job::ProcessImage { path } => {
                    self.process_image(&path).await;
                }
            }
        }
    }

    async fn send_email(&self, to: &str, subject: &str, body: &str) {
        tokio::time::sleep(Duration::from_secs(1)).await;
        println!("Email sent to {}", to);
    }

    async fn generate_report(&self, user_id: u64) {
        tokio::time::sleep(Duration::from_secs(2)).await;
        println!("Report generated for user {}", user_id);
    }

    async fn process_image(&self, path: &str) {
        tokio::time::sleep(Duration::from_secs(3)).await;
        println!("Image processed: {}", path);
    }
}

struct JobQueue {
    sender: mpsc::Sender<Job>,
}

impl JobQueue {
    fn new(num_workers: usize) -> Self {
        let (sender, receiver) = mpsc::channel(100);

        // Create worker pool
        for id in 0..num_workers {
            let worker_receiver = receiver.resubscribe();
            let worker = Worker {
                id,
                receiver: worker_receiver,
            };

            tokio::spawn(async move {
                worker.run().await;
            });
        }

        JobQueue { sender }
    }

    async fn enqueue(&self, job: Job) -> Result<(), String> {
        self.sender
            .send(job)
            .await
            .map_err(|_| "Failed to enqueue job".to_string())
    }
}

#[tokio::main]
async fn main() {
    let queue = JobQueue::new(4);

    // Enqueue jobs
    queue.enqueue(Job::SendEmail {
        to: "user@example.com".to_string(),
        subject: "Hello".to_string(),
        body: "Test email".to_string(),
    }).await.unwrap();

    queue.enqueue(Job::GenerateReport { user_id: 123 }).await.unwrap();

    tokio::time::sleep(Duration::from_secs(10)).await;
}

8.5 Best Practices Checklist

Production-Ready Rust Projects

8.6 Deployment Strategies

Docker Container

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

FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y libssl3 ca-certificates
COPY --from=builder /app/target/release/myapp /usr/local/bin/myapp
CMD ["myapp"]

GitHub Actions CI

# .github/workflows/ci.yml
name: CI

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions-rs/toolchain@v1
        with:
          toolchain: stable
      - name: Run tests
        run: cargo test --all-features
      - name: Run clippy
        run: cargo clippy -- -D warnings
      - name: Check formatting
        run: cargo fmt -- --check

8.7 Chapter Summary

Key Takeaways

8.8 Next Steps

Continue Your Rust Journey