rust-fullstack - 背景
歴史的経緯
Webアプリケーションの進化
- CGI
- サーバーサイドレンダリング - XMLHttpRequest
- 非同期通信 - React/Angular/Vue
- クライアントサイドルーティング - 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()
}
}