References: Rust API Guidelines, Rust Async Book, tokio.rs, Axum official examples, Effective Rust
Scope of Application: Backend web services (Axum + Tokio route). CLI/wasm/embedded are not covered by this specification.
Toolchain
# 格式化
cargo fmt --all
# 静态检查,warning 当 error
cargo clippy --all-targets -- -D warnings
# 测试
cargo test --all-features
# CVE 扫描(独立安装)
cargo audit
# 许可证/重复依赖检查(独立安装)
cargo deny check
# 检测未使用依赖
cargo machete
edition: New projects default to edition = "2024", old projects keep 2021 without forced migration.
Version Baseline(hardcoded to avoid repeated decision-making):
- Tokio
1.x,Axum0.8,tower-http0.6 - serde
1、tracing0.1、tracing-subscriber0.3 - DB:sqlx
0.8(runtime-tokio-rustls)
Naming Conventions
| Type | Rule | Example |
|---|---|---|
| Module/File | snake_case | user_service.rs |
| Function/Variable | snake_case | fetch_user, db_pool |
| Type/Trait/Enum | UpperCamelCase | UserRepo, AppError |
| Constant/Static | SCREAMING_SNAKE_CASE | MAX_RETRY |
| Generic | Single uppercase letter or UpperCamelCase | T, Req, Resp |
| Lifetime | Short lowercase | 'a, 'ctx |
Prohibited:common、util、helper、base and other meaningless module names. Modules are named by responsibility (api、db、service)。
Module Organization
Refer to apple-store-price-rs / org-site-backend for empirically-driven splitting, split files by responsibility,do not pile up lib.rs:
src/
├── main.rs # 仅 bootstrap:日志初始化、读配置、建路由、启服务
├── config.rs # 配置加载(env / toml)
├── error.rs # AppError + IntoResponse impl
├── db.rs # 数据库连接池/迁移
├── api.rs # 路由聚合 + handler
├── service/ # 业务逻辑(无副作用层)
│ └── user.rs
└── models.rs # 领域类型 + serde 派生
Visibility:pub(crate) takes precedence over pub. Only use pub。
Error Handling (Core)
Choose a side by scenario:
| Scenario | Choice | Reason |
|---|---|---|
| Library crate | thiserror |
Callers need to match by variant |
| Binary application top level | anyhow |
Only care about "success/failure + context" |
| Cross-boundary (handler / public API) | Custom AppError + IntoResponse |
Control the external error shape |
Prohibitedin production code:
.unwrap()/.expect()(only available in tests andconstcontexts)panic!()(only for "logically unreachable" cases and with a SAFETY comment)
clippy configuration (in Cargo.toml or clippy.toml):
[lints.clippy]
unwrap_used = "deny"
expect_used = "deny"
panic = "deny"
Use ?for error propagation, use From across types to implement automatic conversion:
#[derive(thiserror::Error, Debug)]
pub enum AppError {
#[error("db error: {0}")]
Db(#[from] sqlx::Error),
#[error("not found")]
NotFound,
#[error("bad request: {0}")]
BadRequest(String),
}
See references/async-axum.md the IntoResponse implementation.
Async Programming Red Lines
1. Blocking calls must be offloaded to spawn_blocking
// ❌ 在 async fn 里直接调用阻塞 API
let data = std::fs::read("big.bin")?;
// ✅
let data = tokio::task::spawn_blocking(|| std::fs::read("big.bin")).await??;
Criteria: CPU-intensive > 10ms,std::fs、std::thread::sleep, synchronous DB driver,reqwest::blocking — all must spawn_blocking。
2. Do not hold .await across std::sync::Mutex
// ❌ 编译能过,运行时死锁/性能崩溃
let g = std_mutex.lock().unwrap();
do_async().await;
// ✅ 用 tokio::sync::Mutex
let g = tokio_mutex.lock().await;
do_async().await;
3. Rc / RefCell cannot cross .await: violates the Send boundary, compilation error, switch to Arc / Mutex。
4. tokio::select! branches must be cancel-safe
- Cancel-safe ✅:
tokio::time::sleep、channel.recv、tokio::io::AsyncReadmarked methods - Not cancel-safe ❌: self-implemented
async fn(may lose data on cancellation)
When unsure, save state to variables outside select! , and branches only read and write local variables.
5. Graceful shutdown: use tokio::signal::ctrl_c() + axum::serve(...).with_graceful_shutdown()。
More pitfalls see references/async-axum.md。
Ownership and Borrowing Minimal Set
Function parameters:
| Want | Use | Do not use |
|---|---|---|
| Read-only string | &str |
&String |
| Read-only slice | &[T] |
&Vec<T> |
| Path | &Path |
&PathBuf / &str |
| String parameters may come from multiple sources | impl AsRef<str> |
Multiple overloads |
Shared decision tree:
需要跨线程共享?
├── 否 → 用 Box<T> 或值传递
└── 是 → Arc<T>
├── 不可变共享 → Arc<T>
├── 读多写少 → Arc<RwLock<T>>
└── 读写均衡 → Arc<Mutex<T>>
Clone is not a bad thing, but inside a loop Clone large objects require vigilance, consider Arc or Cow<'_, T>。
Axum practical key points
type AppState = Arc<InnerState>;
let app = Router::new()
.nest("/api/users", user::routes()) // 按模块拆 + nest
.layer(TraceLayer::new_for_http()) // 日志中间件
.layer(TimeoutLayer::new(Duration::from_secs(30)))
.with_state(state);
- State should always be
Arcwrapped, do not clone bare structs - Handler error type implements
IntoResponse, unify the external form (status + json body) - Split routes by module
fn routes() -> Router<AppState>, avoidmain.rsbloat - Middleware goes through
tower-http: CORS / timeout / rate limiting / logging / static files
See references/async-axum.md。
Dependency management
Version specification:Cargo.toml defaults to caret ("1" equivalent to ^1), do not pin =1.2.3 unless there is a compatibility issue.
Declare features explicitly, do not blindly rely on defaults:
# ✅ 显式开启需要的 feature
tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal"] }
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
Choose rustlsfor TLS, avoid openssl(cross-platform compilation is troublesome).
Dry-run before upgrading:
cargo update --dry-run
# 独立安装
cargo outdated
Cargo.lock Binary projects must commit it, library projects do not commit it.
Testing conventions
// 异步测试用 #[tokio::test]
#[tokio::test]
async fn fetches_user() {
let svc = test_service().await;
let user = svc.find(1).await.unwrap(); // 测试代码可以 .unwrap()
assert_eq!(user.name, "alice");
}
// 表驱动:可选用 rstest crate
#[rstest::rstest]
#[case(1, 2, 3)]
#[case(-1, 1, 0)]
fn adds(#[case] a: i32, #[case] b: i32, #[case] expected: i32) {
assert_eq!(a + b, expected);
}
- Unit tests:
#[cfg(test)] mod tests { ... }stay close to the code under test - Integration tests:
tests/at the top-level directory, each file is its own crate - Tests may allow
.unwrap()(failure means the test fails, which is exactly expected)
unsafe red lines
Business code should in principle prohibit unsafe。
If it must be used (FFI / extreme performance scenarios):
- There must be a
// SAFETY: ...comment before the block, explaining the invariants maintained - Shrink the
unsafeblock to the minimum scope - Prefer to encapsulate it into a safe API exposed externally
- Go through code review, mark it separately for review
Performance key points
| Pitfall | Solution |
|---|---|
Inside the loop format! / string concatenation |
Use String::with_capacity + write! |
| Frequent Vec reallocation | Vec::with_capacity(n) |
Inside the loop .clone() large objects |
Borrow or Arc |
Box<dyn Trait> abuse |
Prefer generics <T: Trait>(static dispatch) |
String as key |
Prefer &str or Cow<'static, str> |
| Blocking IO inside async | spawn_blocking(see the async chapter) |
Performance analysis:
cargo build --release
# 火焰图(独立安装)
cargo flamegraph
# criterion 基准测试
cargo bench
Detailed reference
| File | Content |
|---|---|
references/rust-style.md |
Naming details,From/TryFrom conventions, newtype pattern, iterator preference |
references/async-axum.md |
spawn_blocking boundaries, select! cancellation safety, tower middleware composition, IntoResponse pattern, shutdown signal |
📋 This reply follows:
rust-dev- [specific section]