rust-project · git:20251217.538dc8a · 2025-12-17 · sha256 49bde29c99aed7b1
rust-project git:20251217.538dc8aA
Immutable. This exact content is served forever at /api/v1/blob/49bde29c99aed7b1.
---
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) |
| CLI | Clap (derive) |
| Error (lib) | thiserror |
| Error (app) | anyhow |
| Logging | tracing + tracing-subscriber |
| HTTP Client | reqwest |
| Config | config-rs |
### 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 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?;
// 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 router;
pub mod services;
use std::sync::Arc;
pub struct AppState {
pub db: sqlx::PgPool,
}
impl AppState {
pub fn new(db: sqlx::PgPool) -> Arc<Self> {
Arc::new(Self { db })
}
}
```
### error.rs — Error Handling
```rust
// src/error.rs
use axum::{http::StatusCode, response::{IntoResponse, Response}, Json};
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),
}
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};
pub async fn get_user(
State(state): State<Arc<AppState>>,
Path(id): Path<i64>,
) -> Result<Json<User>> {
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>> {
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};
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)
.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?;
Ok(user)
}
```
---
## 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"] }
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
db-migrate:
sqlx migrate run
db-prepare:
cargo sqlx prepare
```
---
## 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