rust-project · diff
git:20251217.538dc8a to git:20251219.daf1c5a
65 added, 33 removed. Audit A to A.
---
name: rust-project
description: Modern Rust project architecture guide for 2025. Use when creating Rust projects (CLI, web services, libraries). Covers workspace structure, error handling, async patterns, and idiomatic Rust best practices.
---
# Rust Project Architecture
## Core Principles
- **Ownership-first** — Embrace borrow checker, no unnecessary clones
- **Zero-cost abstractions** — Newtype, iterators, async/await
- **Workspace for scale** — Use Cargo workspace for multi-crate projects
- **Error precision** — thiserror for libs, anyhow for apps
- **Async with Tokio** — Tokio runtime + tracing for observability
- **No backwards compatibility** — Delete, don't deprecate. Change directly
- **LiteLLM for LLM APIs** — Use LiteLLM proxy for all LLM integrations
---
## No Backwards Compatibility
> **Delete unused code. Change directly. No compatibility layers.**
```rust
// ❌ 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) { ... }
```
---
## LiteLLM for LLM APIs
> **Use LiteLLM proxy. Don't call provider APIs directly.**
```rust
// 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()?;
```
---
## Quick Start
### 1. Initialize Project
```bash
# Simple project
cargo new myapp
cd myapp
# Workspace project
mkdir myapp && cd myapp
cargo init --name app
```
### 2. Apply Tech Stack
| Layer | Recommendation |
|-------|----------------|
| Async Runtime | Tokio |
| Web Framework | Axum |
| Serialization | Serde |
- | Database | SQLx (compile-time checked) |
+ | ORM / Database | SeaORM (async, Active Record) |
| CLI | Clap (derive) |
| Error (lib) | thiserror |
| Error (app) | anyhow |
| Logging | tracing + tracing-subscriber |
| HTTP Client | reqwest |
| Config | config-rs |
+ ### Web Framework Selection
+
+ | Framework | Choose When |
+ |-----------|-------------|
+ | **Axum** (default) | Modern microservices, Tokio ecosystem, container deployment, Tower middleware |
+ | Actix Web | Maximum throughput, WebSocket-heavy, mature ecosystem needed |
+ | Rocket | Rapid prototyping, small teams, minimal boilerplate |
+
+ > Axum provides the best balance of performance, ergonomics, and Tokio integration for most projects.
+
+ ### Database / ORM Selection
+
+ | Library | Choose When |
+ |---------|-------------|
+ | **SeaORM** (default) | CRUD-heavy services, rapid development, async-first, cross-database testing |
+ | SQLx | Raw SQL control, maximum performance, compile-time SQL validation |
+ | Diesel | Compile-time type safety, stable schema, synchronous workloads |
+
+ > SeaORM is recommended for its Active Record ergonomics, native async support, and seamless Axum integration.
+
### Version Strategy
> **Always use latest. Never pin in templates.**
```toml
[dependencies]
tokio = { version = "*", features = ["full"] }
axum = "*"
serde = { version = "*", features = ["derive"] }
# cargo update fetches latest compatible versions
# Cargo.lock ensures reproducible builds
```
### 3. Choose Project Structure
#### Simple Project (Single Crate)
```
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
```
#### Workspace Project (Multi-Crate)
```
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
```
---
## Architecture Layers
### main.rs — Entry Point
Wire dependencies, start runtime. No business logic.
```rust
// 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()?;
- // Initialize infrastructure
- let db = myapp::db::connect(&config.database_url).await?;
+ // 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 — Library Root
Re-export public API, define AppState.
```rust
// src/lib.rs
pub mod config;
pub mod db;
pub mod error;
pub mod handlers;
- pub mod models;
+ pub mod models; // SeaORM entities
pub mod router;
pub mod services;
+ use sea_orm::DatabaseConnection;
use std::sync::Arc;
pub struct AppState {
- pub db: sqlx::PgPool,
+ pub db: DatabaseConnection,
}
impl AppState {
- pub fn new(db: sqlx::PgPool) -> Arc<Self> {
+ pub fn new(db: DatabaseConnection) -> Arc<Self> {
Arc::new(Self { db })
}
}
```
### error.rs — Error Handling
```rust
// 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")]
- Database(#[from] sqlx::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 Layer
```rust
// src/handlers/user.rs
use axum::{extract::{Path, State}, Json};
use std::sync::Arc;
- use crate::{error::Result, models::User, services, AppState};
+ 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>> {
+ ) -> 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>> {
+ ) -> Result<Json<user::Model>> {
let user = services::user::create(&state.db, input).await?;
Ok(Json(user))
}
```
### services/ — Business Logic
```rust
// src/services/user.rs
- use sqlx::PgPool;
- use crate::{error::{AppError, Result}, models::User};
+ use sea_orm::{ActiveModelTrait, DatabaseConnection, EntityTrait, Set};
+ use crate::{error::{AppError, Result}, models::user};
- pub async fn find_by_id(db: &PgPool, id: i64) -> Result<User> {
- sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", id)
- .fetch_optional(db)
+ 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: &PgPool, input: CreateUserInput) -> Result<User> {
- let user = sqlx::query_as!(
- User,
- r#"INSERT INTO users (email, name) VALUES ($1, $2) RETURNING *"#,
- input.email,
- input.name
- )
- .fetch_one(db)
- .await?;
+ 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)))
+ }
```
---
## Workspace Configuration
```toml
# 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 = "*"
- sqlx = { version = "*", features = ["runtime-tokio", "postgres"] }
+ sea-orm = { version = "*", features = ["sqlx-postgres", "runtime-tokio-native-tls"] }
thiserror = "*"
anyhow = "*"
tracing = "*"
tracing-subscriber = "*"
```
```toml
# 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 Application
```rust
// 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(())
}
```
---
## Testing
```rust
// 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("test@example.com").is_ok());
assert!(validate_email("invalid").is_err());
}
}
```
---
## Makefile
```makefile
.PHONY: build run test lint check clean
build:
cargo build --release
run:
cargo run
dev:
cargo watch -x run
test:
cargo test
test-coverage:
cargo tarpaulin --out Html
lint:
cargo clippy -- -D warnings
fmt:
cargo fmt
check: fmt lint test
@echo "All checks passed!"
clean:
cargo clean
- # Database (SQLx)
- db-create:
- sqlx database create
-
+ # Database (SeaORM)
db-migrate:
- sqlx migrate run
+ sea-orm-cli migrate up
- db-prepare:
- cargo sqlx prepare
+ db-generate:
+ sea-orm-cli generate entity -o src/models
+
+ db-fresh:
+ sea-orm-cli migrate fresh
```
---
## Checklist
```markdown
## Project Setup
- [ ] Cargo.toml configured
- [ ] Workspace structure (if multi-crate)
- [ ] Edition 2024 / resolver = "3"
## Architecture
- [ ] main.rs: only wiring + startup
- [ ] lib.rs: re-exports + AppState
- [ ] error.rs: thiserror types
- [ ] handlers/ services/ models/ separation
## Quality
- [ ] tracing for logging
- [ ] clippy warnings as errors
- [ ] cargo fmt enforced
- [ ] Tests for critical paths
## CI
- [ ] cargo check
- [ ] cargo clippy
- [ ] cargo test
- [ ] cargo fmt --check
```
---
## See Also
- [reference/architecture.md](reference/architecture.md) — Workspace and module patterns
- [reference/tech-stack.md](reference/tech-stack.md) — Crate comparisons
- [reference/patterns.md](reference/patterns.md) — Builder, Newtype, Error patterns