Rust API 服务 — 项目 CLAUDE.md¶
使用 Axum、PostgreSQL、Docker 构建的 Rust API 服务的真实示例。 请将其复制到项目根目录,并根据服务进行自定义。
项目概览¶
栈: Rust 1.78+, Axum(Web 框架), SQLx(异步数据库), PostgreSQL, Tokio(异步运行时), Docker
架构: 具有处理器 → 服务 → 仓库分层的分层架构。HTTP 使用 Axum,编译时类型检查的 SQL 使用 SQLx,横切关注点使用 Tower 中间件。
重要规则¶
Rust 约定¶
- 库错误使用
thiserror,anyhow仅用于二进制 crate 或测试 - 生产代码中不要使用
.unwrap()或.expect()— 使用?传播错误 - 函数参数优先使用
&str而不是String,当所有权转移时返回String - 使用
#![deny(clippy::all, clippy::pedantic)]运行clippy— 修复所有警告 - 为所有公开类型派生
Debug,仅在需要时派生Clone、PartialEq - 除非有
// SAFETY:注释说明理由,否则不要使用unsafe块
数据库¶
- 所有查询使用 SQLx 的
query!或query_as!宏 — 在编译时针对模式进行验证 migrations/中的迁移使用sqlx migrate— 不要直接修改数据库- 使用
sqlx::Pool<Postgres>作为共享状态 — 不要为每个请求创建连接 - 所有查询使用参数化占位符(
$1,$2)— 禁止字符串格式化
// 悪い例: 文字列補間(SQLインジェクションリスク)
let q = format!("SELECT * FROM users WHERE id = '{}'", id);
// 良い例: パラメータ化クエリ、コンパイル時チェック済み
let user = sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", id)
.fetch_optional(&pool)
.await?;
错误处理¶
- 使用
thiserror为每个模块定义领域错误枚举 - 使用
IntoResponse将错误映射为 HTTP 响应 — 不公开内部细节 - 结构化日志使用
tracing— 不要使用println!或eprintln!
use thiserror::Error;
#[derive(Debug, Error)]
pub enum AppError {
#[error("Resource not found")]
NotFound,
#[error("Validation failed: {0}")]
Validation(String),
#[error("Unauthorized")]
Unauthorized,
#[error(transparent)]
Internal(#[from] anyhow::Error),
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let (status, message) = match &self {
Self::NotFound => (StatusCode::NOT_FOUND, self.to_string()),
Self::Validation(msg) => (StatusCode::BAD_REQUEST, msg.clone()),
Self::Unauthorized => (StatusCode::UNAUTHORIZED, self.to_string()),
Self::Internal(err) => {
tracing::error!(?err, "internal error");
(StatusCode::INTERNAL_SERVER_ERROR, "Internal error".into())
}
};
(status, Json(json!({ "error": message }))).into_response()
}
}
测试¶
- 在每个源文件内的
#[cfg(test)]模块中编写单元测试 - 在
tests/目录中编写使用真实 PostgreSQL(Testcontainers 或 Docker)的集成测试 - 对于带有自动迁移和回滚的数据库测试,使用
#[sqlx::test] - 外部服务 mock 使用
mockall或wiremock
代码风格¶
- 最大行宽:100 字符(由 rustfmt 强制)
- 导入分组:
std、外部 crate、crate/super— 用空行分隔 - 模块:每个模块一个文件,
mod.rs仅用于再导出 - 类型:PascalCase,函数/变量:snake_case,常量:UPPER_SNAKE_CASE
文件结构¶
src/
main.rs # エントリーポイント、サーバーセットアップ、グレースフルシャットダウン
lib.rs # 統合テスト用の再エクスポート
config.rs # envyまたはfigmentによる環境設定
router.rs # すべてのルートを持つAxumルーター
middleware/
auth.rs # JWT抽出とバリデーション
logging.rs # リクエスト/レスポンスのトレーシング
handlers/
mod.rs # ルートハンドラー(薄く — サービスに委任)
users.rs
orders.rs
services/
mod.rs # ビジネスロジック
users.rs
orders.rs
repositories/
mod.rs # データベースアクセス(SQLxクエリ)
users.rs
orders.rs
domain/
mod.rs # ドメイン型、エラーenum
user.rs
order.rs
migrations/
001_create_users.sql
002_create_orders.sql
tests/
common/mod.rs # 共有テストヘルパー、テストサーバーセットアップ
api_users.rs # ユーザーエンドポイントの統合テスト
api_orders.rs # 注文エンドポイントの統合テスト
主要模式¶
处理器(薄)¶
async fn create_user(
State(ctx): State<AppState>,
Json(payload): Json<CreateUserRequest>,
) -> Result<(StatusCode, Json<UserResponse>), AppError> {
let user = ctx.user_service.create(payload).await?;
Ok((StatusCode::CREATED, Json(UserResponse::from(user))))
}
服务(业务逻辑)¶
impl UserService {
pub async fn create(&self, req: CreateUserRequest) -> Result<User, AppError> {
if self.repo.find_by_email(&req.email).await?.is_some() {
return Err(AppError::Validation("Email already registered".into()));
}
let password_hash = hash_password(&req.password)?;
let user = self.repo.insert(&req.email, &req.name, &password_hash).await?;
Ok(user)
}
}
仓库(数据访问)¶
impl UserRepository {
pub async fn find_by_email(&self, email: &str) -> Result<Option<User>, sqlx::Error> {
sqlx::query_as!(User, "SELECT * FROM users WHERE email = $1", email)
.fetch_optional(&self.pool)
.await
}
pub async fn insert(
&self,
email: &str,
name: &str,
password_hash: &str,
) -> Result<User, sqlx::Error> {
sqlx::query_as!(
User,
r#"INSERT INTO users (email, name, password_hash)
VALUES ($1, $2, $3) RETURNING *"#,
email, name, password_hash,
)
.fetch_one(&self.pool)
.await
}
}
集成测试¶
#[tokio::test]
async fn test_create_user() {
let app = spawn_test_app().await;
let response = app
.client
.post(&format!("{}/api/v1/users", app.address))
.json(&json!({
"email": "alice@example.com",
"name": "Alice",
"password": "securepassword123"
}))
.send()
.await
.expect("Failed to send request");
assert_eq!(response.status(), StatusCode::CREATED);
let body: serde_json::Value = response.json().await.unwrap();
assert_eq!(body["email"], "alice@example.com");
}
#[tokio::test]
async fn test_create_user_duplicate_email() {
let app = spawn_test_app().await;
// 最初のユーザーを作成
create_test_user(&app, "alice@example.com").await;
// 重複を試みる
let response = create_user_request(&app, "alice@example.com").await;
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
}
环境变量¶
# サーバー
HOST=0.0.0.0
PORT=8080
RUST_LOG=info,tower_http=debug
# データベース
DATABASE_URL=postgres://user:pass@localhost:5432/myapp
# 認証
JWT_SECRET=your-secret-key-min-32-chars
JWT_EXPIRY_HOURS=24
# 任意
CORS_ALLOWED_ORIGINS=http://localhost:3000
测试策略¶
# すべてのテストを実行
cargo test
# 出力付きで実行
cargo test -- --nocapture
# 特定のテストモジュールを実行
cargo test api_users
# カバレッジチェック(cargo-llvm-covが必要)
cargo llvm-cov --html
open target/llvm-cov/html/index.html
# リント
cargo clippy -- -D warnings
# フォーマットチェック
cargo fmt -- --check
ECC 工作流¶
# 計画
/plan "Add order fulfillment with Stripe payment"
# TDDによる開発
/tdd # cargo test ベースのTDDワークフロー
# レビュー
/code-review # Rust固有のコードレビュー
/security-scan # 依存関係監査 + unsafeスキャン
# 検証
/verify # ビルド、clippy、テスト、セキュリティスキャン
Git 工作流¶
feat:新功能、fix:修复缺陷、refactor:代码变更- 从
main切出功能分支,需要 PR - CI:
cargo fmt --check、cargo clippy、cargo test、cargo audit - 部署:基于
scratch或distroless的 Docker 多阶段构建
本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 ev/ECC