rust-project について
単一クレートのアプリから複数クレートのワークスペースまでを対象とする、Rust プロジェクトのアーキテクチャと技術選定ガイドです。非同期ランタイム、Web フレームワーク、データベース、エラー処理、レイヤー構成を例とともに扱います。
主な機能
- 01Rust の設計原則と技術選定
- 02単一クレートとワークスペースの構成例
- 03Tokio の非同期処理と tracing の指針
- 04エラー処理とレイヤー設計パターン
- 05Web、データベース、CLI の参考情報
活用シーン
- 01Rust サービスや CLI の構成を計画
- 02非同期 Web サービスのコンポーネント選定
- 03複数クレートのワークスペースと層構成の整理
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ majiayu000/ spellbook --skill rust-projectインストール後、Claude Code で/rust-projectを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
reference/architecture.mdextended.mdpatterns.mdtech-stack.mdtemplates/cli/Cargo.tomltemplates/cli/src/error.rsmain.rstemplates/cli/src/commands/mod.rsprocess.rstemplates/web-service/.env.exampleCargo.tomlMakefiletemplates/web-service/config/default.tomltemplates/web-service/migrations/001_init.sqltemplates/web-service/src/config.rsdb.rserror.rslib.rsmain.rsrouter.rstemplates/web-service/src/handlers/health.rsmod.rsuser.rstemplates/web-service/src/models/mod.rsuser.rstemplates/web-service/src/services/mod.rsuser.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) |
| CLI | Clap(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 に移動しました。移動された例、コマンド一覧、チェックリスト、プラットフォームの詳細、または実装テンプレートが必要なタスクでは、このリファレンスを読み込んでください。
コメント
Markdown 対応