mcpField
R
rust-project

rust-project

github.com/majiayu000開発28884 回インストール今日更新
リポジトリ

rust-project について

単一クレートのアプリから複数クレートのワークスペースまでを対象とする、Rust プロジェクトのアーキテクチャと技術選定ガイドです。非同期ランタイム、Web フレームワーク、データベース、エラー処理、レイヤー構成を例とともに扱います。

主な機能

  1. 01Rust の設計原則と技術選定
  2. 02単一クレートとワークスペースの構成例
  3. 03Tokio の非同期処理と tracing の指針
  4. 04エラー処理とレイヤー設計パターン
  5. 05Web、データベース、CLI の参考情報

活用シーン

  1. 01Rust サービスや CLI の構成を計画
  2. 02非同期 Web サービスのコンポーネント選定
  3. 03複数クレートのワークスペースと層構成の整理

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/majiayu000/spellbook --skill rust-project

インストール後、Claude Code で/rust-projectを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

reference/
architecture.md
extended.md
patterns.md
tech-stack.md
templates/cli/
Cargo.toml
templates/cli/src/
error.rs
main.rs
templates/cli/src/commands/
mod.rs
process.rs
templates/web-service/
.env.example
Cargo.toml
Makefile
templates/web-service/config/
default.toml
templates/web-service/migrations/
001_init.sql
templates/web-service/src/
config.rs
db.rs
error.rs
lib.rs
main.rs
router.rs
templates/web-service/src/handlers/
health.rs
mod.rs
user.rs
templates/web-service/src/models/
mod.rs
user.rs
templates/web-service/src/services/
mod.rs
user.rs

詳細

Rust プロジェクトのアーキテクチャ

基本原則

  • 所有権を第一に — 借用チェッカーを活用し、不要なクローンを避けます
  • ゼロコスト抽象化 — Newtype、イテレーター、async/await
  • 大規模化に備えたワークスペース — 複数クレートのプロジェクトでは Cargo ワークスペースを使用します
  • エラーの明確化 — ライブラリでは thiserror、アプリでは anyhow を使用します
  • Tokio による非同期処理 — 可観測性のために Tokio ランタイムと tracing を使用します
  • 後方互換性は維持しない — 非推奨にするのではなく削除します。直接変更します
  • LLM API には LiteLLM — すべての LLM 統合に LiteLLM プロキシを使用します

後方互換性は維持しない

未使用のコードは削除します。直接変更します。互換性レイヤーは設けません。

// ❌ BAD: Deprecated attribute kept around
#[deprecated(since = "0.2.0", note = "Use new_function instead")]
pub fn old_function() { ... }

// ❌ BAD: Type alias for renamed types
pub type OldName = NewName; // "for backwards compatibility"

// ❌ BAD: Unused parameters
fn process(_legacy: &str, data: &Data) { ... }

// ❌ BAD: Feature flags for old behavior
#[cfg(feature = "legacy")]
fn old_impl() { ... }

// ✅ GOOD: Just delete and update all usages
pub fn new_function() { ... }
// Then: Find & replace all old_function → new_function

// ✅ GOOD: Remove unused parameters entirely
fn process(data: &Data) { ... }

LLM API には LiteLLM

LiteLLM プロキシを使用します。プロバイダーの API を直接呼び出さないでください。

// src/llm.rs
use async_openai::{Client, config::OpenAIConfig};

pub fn create_client(base_url: &str, api_key: &str) -> Client<OpenAIConfig> {
    let config = OpenAIConfig::new()
        .with_api_base(base_url)  // LiteLLM proxy URL
        .with_api_key(api_key);
    Client::with_config(config)
}

// Usage: connect to LiteLLM, use any model
let client = create_client("http://localhost:4000", &api_key);
let request = CreateChatCompletionRequestArgs::default()
    .model("gpt-4o")  // or "claude-3-opus", "gemini-pro", etc.
    .messages(vec![...])
    .build()?;

クイックスタート

1. プロジェクトを初期化する
# Simple project
cargo new myapp
cd myapp

# Workspace project
mkdir myapp && cd myapp
cargo init --name app
2. 技術スタックを適用する
レイヤー推奨
非同期ランタイムTokio
Web フレームワークAxum
シリアライゼーションSerde
ORM / データベースSeaORM(非同期、Active Record)
CLIClap(derive)
エラー(ライブラリ)thiserror
エラー(アプリ)anyhow
ロギングtracing + tracing-subscriber
HTTP クライアントreqwest
設定config-rs
Web フレームワークの選定
フレームワーク選択するケース
Axum(デフォルト)モダンなマイクロサービス、Tokio エコシステム、コンテナへのデプロイ、Tower ミドルウェア
Actix Web最大限のスループット、WebSocket を多用する場合、成熟したエコシステムが必要な場合
Rocket迅速なプロトタイピング、小規模チーム、ボイラープレートを最小限にしたい場合

ほとんどのプロジェクトにおいて、Axum はパフォーマンス、使いやすさ、Tokio との統合のバランスに最も優れています。

データベース / ORM の選定
ライブラリ選択するケース
SeaORM(デフォルト)CRUD が中心のサービス、迅速な開発、非同期を前提とする場合、複数データベースでのテスト
SQLx生の SQL による制御、最大限のパフォーマンス、コンパイル時の SQL 検証
Dieselコンパイル時の型安全性、安定したスキーマ、同期処理のワークロード

SeaORM は、Active Record の使いやすさ、ネイティブな非同期サポート、Axum とのシームレスな統合により推奨されます。

バージョン戦略

常に最新を使用します。テンプレートではバージョンを固定しないでください。

[dependencies]
tokio = { version = "*", features = ["full"] }
axum = "*"
serde = { version = "*", features = ["derive"] }

# cargo update fetches latest compatible versions
# Cargo.lock ensures reproducible builds
3. プロジェクト構成を選ぶ
シンプルなプロジェクト(単一クレート)
myapp/
├── Cargo.toml
├── src/
│   ├── main.rs           # Entry point
│   ├── lib.rs            # Library root (optional)
│   ├── config.rs         # Configuration
│   ├── error.rs          # Error types
│   ├── handlers/         # HTTP handlers (web)
│   │   └── mod.rs
│   ├── services/         # Business logic
│   │   └── mod.rs
│   └── models/           # Domain types
│       └── mod.rs
├── tests/                # Integration tests
│   └── api_test.rs
└── benches/              # Benchmarks
    └── bench.rs
ワークスペースプロジェクト(複数クレート)
myapp/
├── Cargo.toml            # Workspace manifest
├── crates/
│   ├── app/              # Binary crate
│   │   ├── Cargo.toml
│   │   └── src/main.rs
│   ├── core/             # Business logic lib
│   │   ├── Cargo.toml
│   │   └── src/lib.rs
│   └── infra/            # Infrastructure lib
│       ├── Cargo.toml
│       └── src/lib.rs
├── config/
│   └── default.toml
└── Makefile

アーキテクチャのレイヤー

main.rs — エントリーポイント

依存関係を接続し、ランタイムを起動します。ビジネスロジックは含めません。

// src/main.rs
use anyhow::Result;
use sea_orm::Database;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};

#[tokio::main]
async fn main() -> Result<()> {
    // Initialize tracing
    tracing_subscriber::registry()
        .with(tracing_subscriber::fmt::layer())
        .init();

    // Load config
    let config = myapp::config::load()?;

    // Connect to database (SeaORM)
    let db = Database::connect(&config.database_url).await?;

    // Build application state
    let state = myapp::AppState::new(db);

    // Build router
    let app = myapp::router::build(state);

    // Run server
    let listener = tokio::net::TcpListener::bind(&config.listen_addr).await?;
    tracing::info!("listening on {}", config.listen_addr);
    axum::serve(listener, app).await?;

    Ok(())
}
lib.rs — ライブラリのルート

公開 API を再エクスポートし、AppState を定義します。

// src/lib.rs
pub mod config;
pub mod db;
pub mod error;
pub mod handlers;
pub mod models;  // SeaORM entities
pub mod router;
pub mod services;

use sea_orm::DatabaseConnection;
use std::sync::Arc;

pub struct AppState {
    pub db: DatabaseConnection,
}

impl AppState {
    pub fn new(db: DatabaseConnection) -> Arc<Self> {
        Arc::new(Self { db })
    }
}
error.rs — エラー処理
// src/error.rs
use axum::{http::StatusCode, response::{IntoResponse, Response}, Json};
use sea_orm::DbErr;
use serde_json::json;

#[derive(Debug, thiserror::Error)]
pub enum AppError {
    #[error("not found: {0}")]
    NotFound(String),

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

    #[error("unauthorized")]
    Unauthorized,

    #[error("internal error")]
    Internal(#[from] anyhow::Error),

    #[error("database error: {0}")]
    Database(#[from] DbErr),
}

impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        let (status, message) = match &self {
            AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg.clone()),
            AppError::Validation(msg) => (StatusCode::BAD_REQUEST, msg.clone()),
            AppError::Unauthorized => (StatusCode::UNAUTHORIZED, "unauthorized".into()),
            AppError::Internal(_) | AppError::Database(_) => {
                tracing::error!("Internal error: {:?}", self);
                (StatusCode::INTERNAL_SERVER_ERROR, "internal error".into())
            }
        };

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

pub type Result<T> = std::result::Result<T, AppError>;
handlers/ — HTTP レイヤー
// src/handlers/user.rs
use axum::{extract::{Path, State}, Json};
use std::sync::Arc;
use crate::{error::Result, models::user, services, AppState};

pub async fn get_user(
    State(state): State<Arc<AppState>>,
    Path(id): Path<i64>,
) -> Result<Json<user::Model>> {
    let user = services::user::find_by_id(&state.db, id).await?;
    Ok(Json(user))
}

pub async fn create_user(
    State(state): State<Arc<AppState>>,
    Json(input): Json<CreateUserInput>,
) -> Result<Json<user::Model>> {
    let user = services::user::create(&state.db, input).await?;
    Ok(Json(user))
}
services/ — ビジネスロジック
// src/services/user.rs
use sea_orm::{ActiveModelTrait, DatabaseConnection, EntityTrait, Set};
use crate::{error::{AppError, Result}, models::user};

pub async fn find_by_id(db: &DatabaseConnection, id: i64) -> Result<user::Model> {
    user::Entity::find_by_id(id)
        .one(db)
        .await?
        .ok_or_else(|| AppError::NotFound(format!("user {}", id)))
}

pub async fn create(db: &DatabaseConnection, input: CreateUserInput) -> Result<user::Model> {
    let new_user = user::ActiveModel {
        email: Set(input.email),
        name: Set(input.name),
        ..Default::default()
    };

    let user = new_user.insert(db).await?;
    Ok(user)
}

// Find with relations
pub async fn find_with_posts(db: &DatabaseConnection, id: i64) -> Result<(user::Model, Vec<post::Model>)> {
    user::Entity::find_by_id(id)
        .find_with_related(post::Entity)
        .all(db)
        .await?
        .into_iter()
        .next()
        .ok_or_else(|| AppError::NotFound(format!("user {}", id)))
}

ワークスペースの設定

# Cargo.toml (workspace root)
[workspace]
resolver = "3"
members = ["crates/*"]

[workspace.package]
version = "0.1.0"
edition = "2024"
license = "MIT"

[workspace.dependencies]
tokio = { version = "*", features = ["full"] }
axum = "*"
serde = { version = "*", features = ["derive"] }
serde_json = "*"
sea-orm = { version = "*", features = ["sqlx-postgres", "runtime-tokio-native-tls"] }
thiserror = "*"
anyhow = "*"
tracing = "*"
tracing-subscriber = "*"
# crates/app/Cargo.toml
[package]
name = "app"
version.workspace = true
edition.workspace = true

[dependencies]
core.path = "../core"
infra.path = "../infra"
tokio.workspace = true
axum.workspace = true
anyhow.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true

CLI アプリケーション

// src/main.rs
use clap::Parser;
use anyhow::Result;

#[derive(Parser)]
#[command(name = "myapp", version, about)]
struct Cli {
    /// Input file path
    #[arg(short, long)]
    input: PathBuf,

    /// Output format
    #[arg(short, long, default_value = "json")]
    format: OutputFormat,

    /// Verbose output
    #[arg(short, long)]
    verbose: bool,
}

#[derive(Clone, clap::ValueEnum)]
enum OutputFormat {
    Json,
    Yaml,
    Text,
}

fn main() -> Result<()> {
    let cli = Cli::parse();

    if cli.verbose {
        tracing_subscriber::fmt::init();
    }

    // Process input...
    Ok(())
}

テスト

// tests/api_test.rs
use axum::{body::Body, http::{Request, StatusCode}};
use tower::ServiceExt;

#[tokio::test]
async fn test_get_user() {
    let app = create_test_app().await;

    let response = app
        .oneshot(
            Request::builder()
                .uri("/users/1")
                .body(Body::empty())
                .unwrap(),
        )
        .await
        .unwrap();

    assert_eq!(response.status(), StatusCode::OK);
}

// Unit test with mock
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_validate_email() {
        assert!(validate_email("[email protected]").is_ok());
        assert!(validate_email("invalid").is_err());
    }
}

拡張リファレンス

このスキルを簡潔に保つため、## Makefile から始まる詳細な資料は reference/extended.md に移動しました。移動された例、コマンド一覧、チェックリスト、プラットフォームの詳細、または実装テンプレートが必要なタスクでは、このリファレンスを読み込んでください。

コメント