rust-fullstack - 背景

歴史的経緯

Webアプリケーションの進化

  • 静的HTML(1990年代)
- CGI - サーバーサイドレンダリング

  • AJAX時代(2000年代)
- XMLHttpRequest - 非同期通信

  • SPA時代(2010年代)
- React/Angular/Vue - クライアントサイドルーティング

  • WASM時代(2020年代)
- WebAssembly - 非JavaScript言語でのフロントエンド

Rust Webフレームワークの系譜

Iron (2014)
    ↓
Rocket (2016)
    ↓
Actix-web (2017) ← 現在最もポピュラー
    ↓
Axum (2021) ← Tokioチーム公式

フロントエンドフレームワーク

フレームワーク 特徴
Yew React風、最も成熟
Leptos 細粒度リアクティビティ
Dioxus React風、クロスプラットフォーム
Sycamore シグナルベース

コンピュータサイエンス的な意味

WebAssemblyの仕組み

Rustコード → LLVM IR → WASM バイナリ → ブラウザ実行
                         ↓
                    wasm-bindgen
                         ↓
                    JavaScript glue

SPAアーキテクチャ

┌─────────────────────────────────────────────┐
│                 ブラウザ                     │
│  ┌─────────────────────────────────────┐   │
│  │           Rust WASM App             │   │
│  │  ┌─────────┐ ┌─────────┐ ┌───────┐ │   │
│  │  │  State  │ │ Router  │ │ View  │ │   │
│  │  └────┬────┘ └────┬────┘ └───┬───┘ │   │
│  │       └───────────┴──────────┘     │   │
│  └─────────────────────────────────────┘   │
│                    ↕ HTTP                  │
└────────────────────────────────────────────┘
                     ↕
┌────────────────────────────────────────────┐
│               Rust Backend                 │
│  ┌──────────┐ ┌──────────┐ ┌──────────┐  │
│  │  Routes  │ │ Handlers │ │ Services │  │
│  └────┬─────┘ └────┬─────┘ └────┬─────┘  │
│       └────────────┴────────────┘        │
│                    ↕ SQL                  │
│  ┌──────────────────────────────────────┐│
│  │            Database (ORM)            ││
│  └──────────────────────────────────────┘│
└────────────────────────────────────────────┘

認証の仕組み

JWT認証フロー:

1. ログイン
   Client → POST /login {email, password} → Server
   Server → verify password → generate JWT → Client

2. 認証付きリクエスト
   Client → GET /api/tasks [Authorization: Bearer JWT] → Server
   Server → verify JWT → extract user_id → query DB → Client

3. トークン構造
   Header.Payload.Signature
   {alg:HS256}.{user_id,exp}.HMAC(secret)

実践での活用

本番環境アーキテクチャ

                    ┌─────────────┐
                    │   CDN       │
                    │ (静的資産)   │
                    └──────┬──────┘
                           │
┌──────────────────────────┴───────────────────────────┐
│                   Load Balancer                      │
└───────┬─────────────────┬─────────────────┬─────────┘
        │                 │                 │
   ┌────┴────┐       ┌────┴────┐       ┌────┴────┐
   │ App 1   │       │ App 2   │       │ App 3   │
   └────┬────┘       └────┬────┘       └────┬────┘
        │                 │                 │
        └────────────────┬┴─────────────────┘
                         │
                  ┌──────┴──────┐
                  │   Database  │
                  │  (Primary)  │
                  └──────┬──────┘
                         │
                  ┌──────┴──────┐
                  │   Replica   │
                  └─────────────┘

デプロイメント戦略

// Docker マルチステージビルド
// Dockerfile
FROM rust:1.70 as builder
WORKDIR /app
COPY . .
RUN cargo build --release

FROM debian:bullseye-slim
COPY --from=builder /app/target/release/backend /usr/local/bin/
EXPOSE 8080
CMD ["backend"]

環境変数管理

use std::env;

#[derive(Clone)]
pub struct Config {
    pub database_url: String,
    pub jwt_secret: String,
    pub server_host: String,
    pub server_port: u16,
}

impl Config {
    pub fn from_env() -> Self {
        Config {
            database_url: env::var("DATABASE_URL")
                .expect("DATABASE_URL must be set"),
            jwt_secret: env::var("JWT_SECRET")
                .expect("JWT_SECRET must be set"),
            server_host: env::var("SERVER_HOST")
                .unwrap_or_else(|_| "0.0.0.0".into()),
            server_port: env::var("SERVER_PORT")
                .ok()
                .and_then(|s| s.parse().ok())
                .unwrap_or(8080),
        }
    }
}

実世界とのギャップ

WASM の制限

// ブラウザAPIへのアクセスは web-sys 経由
use web_sys::{window, Document};

// ファイルシステムアクセス不可
// std::fs は使用不可

// スレッド制限
// Web Worker + SharedArrayBuffer が必要

// DOM操作
// 直接操作は非推奨、仮想DOMを使用

パフォーマンス考慮

// WASMバンドルサイズ最適化
// Cargo.toml
[profile.release]
opt-level = 'z'     # サイズ最適化
lto = true          # リンク時最適化
codegen-units = 1   # 単一コードユニット
panic = 'abort'     # パニック時即abort

// wasm-opt で追加最適化
// wasm-opt -Oz -o output.wasm input.wasm

認証のベストプラクティス

// パスワードハッシュ化
use argon2::{self, Config};

fn hash_password(password: &str) -> String {
    let salt = generate_salt();
    argon2::hash_encoded(
        password.as_bytes(),
        &salt,
        &Config::default()
    ).unwrap()
}

fn verify_password(password: &str, hash: &str) -> bool {
    argon2::verify_encoded(hash, password.as_bytes())
        .unwrap_or(false)
}

// JWTトークン有効期限
const ACCESS_TOKEN_DURATION: i64 = 15 * 60;      // 15分
const REFRESH_TOKEN_DURATION: i64 = 7 * 24 * 3600; // 7日

エラーハンドリング統一

// アプリケーション全体で統一されたエラー型
#[derive(Debug, thiserror::Error)]
pub enum AppError {
    #[error("認証エラー: {0}")]
    Auth(String),

    #[error("データベースエラー: {0}")]
    Database(#[from] sqlx::Error),

    #[error("バリデーションエラー: {0}")]
    Validation(String),

    #[error("リソースが見つかりません")]
    NotFound,
}

impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        let (status, message) = match self {
            AppError::Auth(_) => (StatusCode::UNAUTHORIZED, self.to_string()),
            AppError::Database(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Internal error".into()),
            AppError::Validation(msg) => (StatusCode::BAD_REQUEST, msg),
            AppError::NotFound => (StatusCode::NOT_FOUND, self.to_string()),
        };

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