
Rust Project
- 77 installs
- 253 repo stars
- Updated August 4, 2026
- majiayu000/claude-arsenal
Helps with ai & agent building tasks during AI-assisted development.
About
rust-project is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- rust-project
- AI & Agent Building
- AI-coding skill
Rust Project by the numbers
- 77 all-time installs (skills.sh)
- Ranked #5,386 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/majiayu000/claude-arsenal --skill rust-projectAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 77 |
|---|---|
| repo stars | ★ 253 |
| Last updated | August 4, 2026 |
| Repository | majiayu000/claude-arsenal ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
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.
// ❌ 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.
// 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
# Simple project
cargo new myapp
cd myapp
# Workspace project
mkdir myapp && cd myapp
cargo init --name app2. Apply Tech Stack
| Layer | Recommendation |
|---|---|
| Async Runtime | Tokio |
| Web Framework | Axum |
| Serialization | Serde |
| 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.
[dependencies]
tokio = { version = "*", features = ["full"] }
axum = "*"
serde = { version = "*", features = ["derive"] }
# cargo update fetches latest compatible versions
# Cargo.lock ensures reproducible builds3. 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.rsWorkspace 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.
// 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 — Library Root
Re-export public API, define 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 — Error Handling
// 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 Layer
// 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/ — Business Logic
// 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)))
}---
Workspace Configuration
# 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 Application
// 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
// 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());
}
}---
Extended Reference
Detailed material starting at ## Makefile has been moved to `reference/extended.md` to keep this skill concise. Load that reference when the task requires the moved examples, command catalogs, checklists, platform details, or implementation templates.
Rust Project Architecture
Module System Fundamentals
Crate vs Module
- Crate: Compilation unit. Either binary (
main.rs) or library (lib.rs) - Module: Logical grouping within a crate. One file = one module
- Package: Contains
Cargo.toml, can have multiple crates
// src/lib.rs - crate root
pub mod config; // loads src/config.rs or src/config/mod.rs
pub mod handlers; // loads src/handlers/mod.rs (directory)
mod internal; // private module
// Re-export for cleaner API
pub use config::Config;
pub use handlers::router;Visibility Rules
pub struct User { // Public struct
pub name: String, // Public field
email: String, // Private field (crate only)
pub(crate) id: i64, // Visible within crate
pub(super) role: Role, // Visible to parent module
}
pub fn create_user() { } // Public function
fn validate() { } // Private (module only)
pub(crate) fn internal() {} // Crate-visible---
Simple Project Structure
Best for: CLI tools, small services, single-purpose libraries.
myapp/
├── Cargo.toml
├── src/
│ ├── main.rs # Binary entry point
│ ├── lib.rs # Library root (optional but recommended)
│ ├── config.rs # Configuration loading
│ ├── error.rs # Error types with thiserror
│ ├── handlers/ # HTTP handlers (for web apps)
│ │ ├── mod.rs # pub mod user; pub mod health;
│ │ ├── user.rs
│ │ └── health.rs
│ ├── services/ # Business logic
│ │ ├── mod.rs
│ │ └── user.rs
│ ├── models/ # Domain types
│ │ ├── mod.rs
│ │ └── user.rs
│ └── db/ # Database layer
│ ├── mod.rs
│ └── queries.rs
├── tests/ # Integration tests
│ └── api_test.rs
├── benches/ # Benchmarks
│ └── perf.rs
└── examples/ # Example usage
└── basic.rsModule Organization
// src/handlers/mod.rs
mod health;
mod user;
pub use health::*;
pub use user::*;
// Or explicit re-exports:
pub use health::health_check;
pub use user::{get_user, create_user, delete_user};---
Workspace Structure
Best for: Large projects, monorepos, multi-binary projects.
myapp/
├── Cargo.toml # Workspace manifest (no [package])
├── Cargo.lock # Single lockfile for all crates
├── crates/
│ ├── app/ # Main binary
│ │ ├── Cargo.toml
│ │ └── src/
│ │ └── main.rs
│ ├── api/ # HTTP API library
│ │ ├── Cargo.toml
│ │ └── src/
│ │ ├── lib.rs
│ │ ├── handlers.rs
│ │ └── router.rs
│ ├── core/ # Core domain logic
│ │ ├── Cargo.toml
│ │ └── src/
│ │ ├── lib.rs
│ │ ├── models.rs
│ │ └── services.rs
│ ├── db/ # Database layer
│ │ ├── Cargo.toml
│ │ └── src/
│ │ ├── lib.rs
│ │ └── repositories.rs
│ └── cli/ # CLI binary (optional)
│ ├── Cargo.toml
│ └── src/
│ └── main.rs
├── config/
│ ├── default.toml
│ └── production.toml
├── migrations/ # SQLx migrations
│ └── 001_init.sql
└── MakefileWorkspace Cargo.toml
[workspace]
resolver = "3"
members = ["crates/*"]
# Shared package metadata
[workspace.package]
version = "0.1.0"
edition = "2024"
authors = ["Your Name <you@example.com>"]
license = "MIT"
repository = "https://github.com/you/myapp"
# Shared dependencies - define once, use everywhere
[workspace.dependencies]
# Async
tokio = { version = "*", features = ["full"] }
async-trait = "*"
# Web
axum = "*"
tower = "*"
tower-http = { version = "*", features = ["cors", "trace"] }
# Serialization
serde = { version = "*", features = ["derive"] }
serde_json = "*"
# Database
sqlx = { version = "*", features = ["runtime-tokio", "tls-native-tls", "postgres", "macros", "migrate"] }
# Error handling
thiserror = "*"
anyhow = "*"
# Observability
tracing = "*"
tracing-subscriber = { version = "*", features = ["env-filter"] }
# Testing
tokio-test = "*"
# Shared lint configuration
[workspace.lints.rust]
unsafe_code = "forbid"
[workspace.lints.clippy]
all = "warn"
pedantic = "warn"Crate Cargo.toml
# crates/app/Cargo.toml
[package]
name = "app"
version.workspace = true
edition.workspace = true
[dependencies]
# Internal crates
api = { path = "../api" }
core = { path = "../core" }
db = { path = "../db" }
# External (inherit from workspace)
tokio.workspace = true
anyhow.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
[lints]
workspace = true---
Dependency Graph
┌─────────────────────────────────────────────────┐
│ app │
│ (binary crate) │
└─────────────────────────────────────────────────┘
│
┌────────────┼────────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ api │ │ cli │ │ worker │
│ (lib) │ │ (binary)│ │ (binary)│
└─────────┘ └─────────┘ └─────────┘
│ │ │
└────────────┼────────────┘
▼
┌─────────┐
│ core │
│ (lib) │
└─────────┘
│
┌────────────┼────────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ db │ │ cache │ │ queue │
│ (lib) │ │ (lib) │ │ (lib) │
└─────────┘ └─────────┘ └─────────┘Rules:
- Lower layers never depend on upper layers
corecontains pure business logic, no I/O- Infrastructure crates (
db,cache) implement traits fromcore - Binary crates wire everything together
---
ripgrep-Style Architecture
For CLI tools processing data with high performance.
myapp/
├── Cargo.toml # Workspace root
├── crates/
│ ├── myapp/ # Main binary + CLI parsing
│ │ ├── Cargo.toml
│ │ └── src/
│ │ ├── main.rs
│ │ └── args.rs # CLI argument parsing
│ ├── myapp-core/ # Facade crate, coordinates others
│ │ ├── Cargo.toml
│ │ └── src/lib.rs
│ ├── myapp-parser/ # Parsing logic
│ │ ├── Cargo.toml
│ │ └── src/lib.rs
│ ├── myapp-matcher/ # Matching/filtering
│ │ ├── Cargo.toml
│ │ └── src/lib.rs
│ └── myapp-printer/ # Output formatting
│ ├── Cargo.toml
│ └── src/lib.rsKey patterns from ripgrep:
- Facade crate (
-core) provides unified API - Each sub-crate has single responsibility
- Parallel processing with work-stealing (crossbeam)
- Arc-wrapped state for shared immutable data
---
Binary + Library Pattern
Expose both binary and library from same crate.
myapp/
├── Cargo.toml
├── src/
│ ├── main.rs # Uses lib.rs
│ └── lib.rs # All logic here# Cargo.toml
[package]
name = "myapp"
[lib]
name = "myapp"
path = "src/lib.rs"
[[bin]]
name = "myapp"
path = "src/main.rs"// src/main.rs
use myapp::Config;
fn main() -> anyhow::Result<()> {
let config = Config::from_env()?;
myapp::run(config)?;
Ok(())
}
// src/lib.rs
pub mod config;
pub use config::Config;
pub fn run(config: Config) -> anyhow::Result<()> {
// ...
}---
Testing Structure
Unit Tests (Same File)
// src/services/user.rs
pub fn validate_email(email: &str) -> bool {
email.contains('@') && email.contains('.')
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_email() {
assert!(validate_email("test@example.com"));
}
#[test]
fn test_invalid_email() {
assert!(!validate_email("invalid"));
}
}Integration Tests (tests/)
// tests/api_test.rs
use myapp::AppState;
#[tokio::test]
async fn test_full_workflow() {
let state = setup_test_state().await;
// Test against real (test) database
}Test Utilities Crate
crates/
├── app/
├── core/
└── test-utils/ # Shared test helpers
├── Cargo.toml
└── src/
├── lib.rs
├── fixtures.rs
└── mocks.rs# crates/app/Cargo.toml
[dev-dependencies]
test-utils = { path = "../test-utils" }---
Feature Flags
[features]
default = ["postgres"]
postgres = ["sqlx/postgres"]
mysql = ["sqlx/mysql"]
sqlite = ["sqlx/sqlite"]
full = ["postgres", "mysql", "sqlite"]#[cfg(feature = "postgres")]
pub mod postgres;
#[cfg(feature = "mysql")]
pub mod mysql;---
Conditional Compilation
// Platform-specific code
#[cfg(target_os = "linux")]
fn get_memory_info() -> MemInfo { /* Linux impl */ }
#[cfg(target_os = "macos")]
fn get_memory_info() -> MemInfo { /* macOS impl */ }
#[cfg(windows)]
fn get_memory_info() -> MemInfo { /* Windows impl */ }
// Test-only code
#[cfg(test)]
fn mock_service() -> MockService { }
// Debug builds only
#[cfg(debug_assertions)]
fn debug_print(msg: &str) { eprintln!("[DEBUG] {}", msg); }rust-project Extended Reference
This file preserves detailed material moved out of SKILL.md for progressive disclosure. Load it only when the current task needs the specific examples, commands, templates, or checklists below.
Moved content starts at: ## 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 (SeaORM)
db-migrate:
sea-orm-cli migrate up
db-generate:
sea-orm-cli generate entity -o src/models
db-fresh:
sea-orm-cli migrate fresh---
Checklist
## 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 — Workspace and module patterns
- reference/tech-stack.md — Crate comparisons
- reference/patterns.md — Builder, Newtype, Error patterns
Rust Design Patterns
Newtype Pattern
Zero-cost wrapper for type safety and encapsulation.
Basic Usage
// Type safety: prevent mixing IDs
struct UserId(i64);
struct OrderId(i64);
fn get_user(id: UserId) -> User { ... }
fn get_order(id: OrderId) -> Order { ... }
// Compiler prevents: get_user(order_id)
let user = get_user(UserId(123));With Validation
use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Email(String);
#[derive(Debug, Error)]
#[error("invalid email: {0}")]
pub struct InvalidEmail(String);
impl Email {
pub fn new(value: impl Into<String>) -> Result<Self, InvalidEmail> {
let value = value.into();
if value.contains('@') && value.contains('.') {
Ok(Self(value))
} else {
Err(InvalidEmail(value))
}
}
pub fn as_str(&self) -> &str {
&self.0
}
}
// Use TryFrom for ergonomic conversion
impl TryFrom<String> for Email {
type Error = InvalidEmail;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::new(value)
}
}With Serde
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(transparent)]
pub struct UserId(i64);
// JSON: just the number, not {"0": 123}Deref for Convenience
use std::ops::Deref;
#[derive(Debug, Clone)]
pub struct NonEmptyString(String);
impl Deref for NonEmptyString {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
// Now you can call str methods directly
let name = NonEmptyString::new("Alice").unwrap();
println!("{}", name.len()); // Works!---
Builder Pattern
Construct complex objects step by step.
Basic Builder
#[derive(Debug)]
pub struct Server {
host: String,
port: u16,
max_connections: usize,
timeout: Duration,
}
#[derive(Default)]
pub struct ServerBuilder {
host: String,
port: u16,
max_connections: usize,
timeout: Duration,
}
impl ServerBuilder {
pub fn new() -> Self {
Self {
host: "127.0.0.1".into(),
port: 8080,
max_connections: 100,
timeout: Duration::from_secs(30),
}
}
pub fn host(mut self, host: impl Into<String>) -> Self {
self.host = host.into();
self
}
pub fn port(mut self, port: u16) -> Self {
self.port = port;
self
}
pub fn max_connections(mut self, n: usize) -> Self {
self.max_connections = n;
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn build(self) -> Server {
Server {
host: self.host,
port: self.port,
max_connections: self.max_connections,
timeout: self.timeout,
}
}
}
// Usage
let server = ServerBuilder::new()
.host("0.0.0.0")
.port(3000)
.max_connections(1000)
.build();Builder with Validation
impl ServerBuilder {
pub fn build(self) -> Result<Server, BuildError> {
if self.port == 0 {
return Err(BuildError::InvalidPort);
}
if self.max_connections == 0 {
return Err(BuildError::InvalidConnections);
}
Ok(Server {
host: self.host,
port: self.port,
max_connections: self.max_connections,
timeout: self.timeout,
})
}
}Mutable Reference Builder
More efficient for reusable builders.
impl ServerBuilder {
pub fn host(&mut self, host: impl Into<String>) -> &mut Self {
self.host = host.into();
self
}
pub fn port(&mut self, port: u16) -> &mut Self {
self.port = port;
self
}
pub fn build(&self) -> Server {
Server {
host: self.host.clone(),
port: self.port,
max_connections: self.max_connections,
timeout: self.timeout,
}
}
}
// Usage
let mut builder = ServerBuilder::new();
builder.host("localhost").port(8080);
let server1 = builder.build();
builder.port(9090);
let server2 = builder.build();derive_builder Crate
[dependencies]
derive_builder = "*"use derive_builder::Builder;
#[derive(Builder, Debug)]
#[builder(setter(into))]
pub struct Server {
host: String,
#[builder(default = "8080")]
port: u16,
#[builder(default = "100")]
max_connections: usize,
}
// Auto-generated ServerBuilder
let server = ServerBuilder::default()
.host("localhost")
.port(3000)
.build()?;---
Error Handling Patterns
Custom Error with thiserror
use thiserror::Error;
#[derive(Debug, Error)]
pub enum AppError {
#[error("user not found: {id}")]
UserNotFound { id: i64 },
#[error("validation failed: {0}")]
Validation(String),
#[error("database error")]
Database(#[from] sqlx::Error),
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("internal error")]
Internal(#[source] anyhow::Error),
}
impl AppError {
pub fn validation(msg: impl Into<String>) -> Self {
Self::Validation(msg.into())
}
}Error Context with anyhow
use anyhow::{Context, Result, bail, ensure};
fn process_config(path: &Path) -> Result<Config> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("failed to read config: {}", path.display()))?;
ensure!(!content.is_empty(), "config file is empty");
let config: Config = toml::from_str(&content)
.context("failed to parse config")?;
if config.port == 0 {
bail!("port cannot be zero");
}
Ok(config)
}Result Extension Trait
pub trait ResultExt<T> {
fn or_not_found(self, resource: &str) -> Result<T, AppError>;
}
impl<T> ResultExt<T> for Option<T> {
fn or_not_found(self, resource: &str) -> Result<T, AppError> {
self.ok_or_else(|| AppError::NotFound(resource.into()))
}
}
// Usage
let user = repo.find_by_id(id).await?.or_not_found("user")?;Error to HTTP Response (Axum)
use axum::{http::StatusCode, response::{IntoResponse, Response}, Json};
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let (status, message) = match &self {
AppError::UserNotFound { .. } => (StatusCode::NOT_FOUND, self.to_string()),
AppError::Validation(_) => (StatusCode::BAD_REQUEST, self.to_string()),
AppError::Database(_) | AppError::Internal(_) => {
tracing::error!(?self, "internal error");
(StatusCode::INTERNAL_SERVER_ERROR, "internal error".into())
}
AppError::Io(_) => (StatusCode::INTERNAL_SERVER_ERROR, "io error".into()),
};
(status, Json(serde_json::json!({ "error": message }))).into_response()
}
}---
Repository Pattern
Abstract data access behind traits.
use async_trait::async_trait;
// Domain model
#[derive(Debug, Clone)]
pub struct User {
pub id: UserId,
pub email: Email,
pub name: String,
}
// Repository trait (defined in domain layer)
#[async_trait]
pub trait UserRepository: Send + Sync {
async fn find_by_id(&self, id: UserId) -> Result<Option<User>>;
async fn find_by_email(&self, email: &Email) -> Result<Option<User>>;
async fn save(&self, user: &User) -> Result<()>;
async fn delete(&self, id: UserId) -> Result<()>;
}
// SQLx implementation
pub struct PgUserRepository {
pool: PgPool,
}
impl PgUserRepository {
pub fn new(pool: PgPool) -> Self {
Self { pool }
}
}
#[async_trait]
impl UserRepository for PgUserRepository {
async fn find_by_id(&self, id: UserId) -> Result<Option<User>> {
let row = sqlx::query_as!(
UserRow,
"SELECT id, email, name FROM users WHERE id = $1",
id.0
)
.fetch_optional(&self.pool)
.await?;
Ok(row.map(User::from))
}
// ... other methods
}In-Memory Implementation (Testing)
use std::collections::HashMap;
use std::sync::RwLock;
pub struct InMemoryUserRepository {
users: RwLock<HashMap<UserId, User>>,
}
impl InMemoryUserRepository {
pub fn new() -> Self {
Self {
users: RwLock::new(HashMap::new()),
}
}
}
#[async_trait]
impl UserRepository for InMemoryUserRepository {
async fn find_by_id(&self, id: UserId) -> Result<Option<User>> {
Ok(self.users.read().unwrap().get(&id).cloned())
}
async fn save(&self, user: &User) -> Result<()> {
self.users.write().unwrap().insert(user.id.clone(), user.clone());
Ok(())
}
// ...
}---
Service Pattern
Business logic with injected dependencies.
pub struct UserService<R: UserRepository> {
repo: R,
}
impl<R: UserRepository> UserService<R> {
pub fn new(repo: R) -> Self {
Self { repo }
}
pub async fn create(&self, input: CreateUserInput) -> Result<User> {
// Validate
let email = Email::new(&input.email)?;
// Check uniqueness
if self.repo.find_by_email(&email).await?.is_some() {
return Err(AppError::validation("email already exists"));
}
// Create
let user = User {
id: UserId::new(),
email,
name: input.name,
};
self.repo.save(&user).await?;
Ok(user)
}
pub async fn get(&self, id: UserId) -> Result<User> {
self.repo
.find_by_id(id)
.await?
.ok_or(AppError::UserNotFound { id: id.0 })
}
}With Arc for Shared State
use std::sync::Arc;
pub struct AppState {
pub user_service: UserService<PgUserRepository>,
}
impl AppState {
pub fn new(pool: PgPool) -> Arc<Self> {
let repo = PgUserRepository::new(pool);
Arc::new(Self {
user_service: UserService::new(repo),
})
}
}
// In Axum
let state = AppState::new(pool);
let app = Router::new()
.route("/users", post(create_user))
.with_state(state);
async fn create_user(
State(state): State<Arc<AppState>>,
Json(input): Json<CreateUserInput>,
) -> Result<Json<User>, AppError> {
let user = state.user_service.create(input).await?;
Ok(Json(user))
}---
Type State Pattern
Compile-time state machine.
// States (zero-sized types)
pub struct Draft;
pub struct Published;
pub struct Archived;
// Document with state parameter
pub struct Document<State> {
id: i64,
title: String,
content: String,
_state: std::marker::PhantomData<State>,
}
impl Document<Draft> {
pub fn new(title: String, content: String) -> Self {
Self {
id: 0,
title,
content,
_state: std::marker::PhantomData,
}
}
pub fn edit(&mut self, content: String) {
self.content = content;
}
pub fn publish(self) -> Document<Published> {
Document {
id: self.id,
title: self.title,
content: self.content,
_state: std::marker::PhantomData,
}
}
}
impl Document<Published> {
// Can't edit published documents!
pub fn archive(self) -> Document<Archived> {
Document {
id: self.id,
title: self.title,
content: self.content,
_state: std::marker::PhantomData,
}
}
}
// Usage
let mut doc = Document::<Draft>::new("Title".into(), "Content".into());
doc.edit("New content".into()); // OK
let published = doc.publish();
// published.edit(...); // Compile error!
let archived = published.archive();---
Extension Trait Pattern
Add methods to foreign types.
pub trait StringExt {
fn truncate_with_ellipsis(&self, max_len: usize) -> String;
}
impl StringExt for str {
fn truncate_with_ellipsis(&self, max_len: usize) -> String {
if self.len() <= max_len {
self.to_string()
} else {
format!("{}...", &self[..max_len.saturating_sub(3)])
}
}
}
// Usage
let title = "Very long title here".truncate_with_ellipsis(10);For Option/Result
pub trait OptionExt<T> {
fn ok_or_not_found(self, msg: &str) -> Result<T, AppError>;
}
impl<T> OptionExt<T> for Option<T> {
fn ok_or_not_found(self, msg: &str) -> Result<T, AppError> {
self.ok_or_else(|| AppError::NotFound(msg.into()))
}
}
// Usage
let user = repo.find(id).await?.ok_or_not_found("user")?;---
From/Into Conversion
// Domain model
pub struct User {
pub id: UserId,
pub email: Email,
pub name: String,
}
// Database row
struct UserRow {
id: i64,
email: String,
name: String,
}
// API response
#[derive(Serialize)]
struct UserResponse {
id: i64,
email: String,
name: String,
}
impl From<UserRow> for User {
fn from(row: UserRow) -> Self {
Self {
id: UserId(row.id),
email: Email::new_unchecked(row.email), // Trust DB data
name: row.name,
}
}
}
impl From<User> for UserResponse {
fn from(user: User) -> Self {
Self {
id: user.id.0,
email: user.email.into_string(),
name: user.name,
}
}
}
// Usage
let user: User = row.into();
let response: UserResponse = user.into();---
Summary Table
| Pattern | Use Case |
|---|---|
| Newtype | Type safety, validation, encapsulation |
| Builder | Complex object construction |
| Repository | Abstract data access |
| Service | Business logic with DI |
| Type State | Compile-time state machines |
| Extension Trait | Add methods to foreign types |
| From/Into | Type conversions |
Rust Tech Stack
Version Strategy
Always use latest. Never pin versions in templates.
[dependencies]
tokio = { version = "*", features = ["full"] }
axum = "*"
serde = { version = "*", features = ["derive"] }cargo updatefetches latest compatible versionsCargo.lockensures reproducible builds- Breaking changes are handled by reading changelogs
- For libraries: use semver ranges (
"1"or">=1.0, <2")
---
Async Runtime
Tokio (Recommended)
The de facto standard for async Rust. Powers Axum, SQLx, reqwest.
[dependencies]
tokio = { version = "*", features = ["full"] }
# Or minimal:
tokio = { version = "*", features = ["rt-multi-thread", "macros"] }#[tokio::main]
async fn main() {
let handle = tokio::spawn(async {
// Background task
});
handle.await.unwrap();
}async-std
Alternative runtime, slightly different API.
[dependencies]
async-std = { version = "*", features = ["attributes"] }When to choose:
- Tokio: Most ecosystem support, production proven
- async-std: Simpler API, closer to std
---
Web Frameworks
Axum (Recommended)
Built by Tokio team. Ergonomic, Tower middleware, type-safe extractors.
[dependencies]
axum = "*"
tower = "*"
tower-http = { version = "*", features = ["cors", "trace"] }use axum::{Router, routing::get, extract::State};
async fn handler(State(db): State<PgPool>) -> impl IntoResponse {
Json(json!({ "status": "ok" }))
}
let app = Router::new()
.route("/health", get(handler))
.with_state(db);Actix Web
Highest raw performance, actor model.
[dependencies]
actix-web = "*"use actix_web::{web, App, HttpServer, Responder};
async fn handler() -> impl Responder {
web::Json(json!({ "status": "ok" }))
}
HttpServer::new(|| App::new().route("/", web::get().to(handler)))
.bind("127.0.0.1:8080")?
.run()
.awaitRocket
Convention over configuration, most ergonomic.
[dependencies]
rocket = "*"Comparison
| Feature | Axum | Actix Web | Rocket |
|---|---|---|---|
| Performance | Excellent | Best | Good |
| Ergonomics | Great | Good | Best |
| Tokio integration | Native | Custom runtime | Tokio |
| Middleware | Tower | Actix | Fairings |
| Learning curve | Medium | Steep | Easy |
| Production use | Growing | Mature | Mature |
Recommendation: Axum for new projects (best balance).
---
Database
SQLx (Recommended)
Compile-time checked SQL, async, pure Rust.
[dependencies]
sqlx = { version = "*", features = ["runtime-tokio", "tls-native-tls", "postgres", "macros", "migrate"] }// Compile-time verified query
let user = sqlx::query_as!(
User,
"SELECT id, name, email FROM users WHERE id = $1",
id
)
.fetch_one(&pool)
.await?;
// Dynamic query
let users = sqlx::query("SELECT * FROM users WHERE active = $1")
.bind(true)
.fetch_all(&pool)
.await?;Key features:
- Compile-time SQL verification
- No DSL, just SQL
- Async-first design
- Automatic migrations
Diesel
Type-safe DSL, synchronous (async via diesel-async).
[dependencies]
diesel = { version = "*", features = ["postgres"] }use diesel::prelude::*;
users::table
.filter(users::active.eq(true))
.load::<User>(&mut conn)?SeaORM
ActiveRecord-style, async, dynamic queries.
[dependencies]
sea-orm = { version = "*", features = ["runtime-tokio-native-tls", "sqlx-postgres"] }Comparison
| Feature | SQLx | Diesel | SeaORM |
|---|---|---|---|
| Query style | Raw SQL | DSL | ActiveRecord |
| Compile-time check | SQL verified | Type-safe DSL | Runtime |
| Async | Native | diesel-async | Native |
| Migrations | Built-in | diesel_migrations | Built-in |
| Learning curve | Easy | Medium | Easy |
Recommendation: SQLx for compile-time safety with raw SQL control.
---
CLI Parsing
Clap (Recommended)
Most feature-complete, derive macros.
[dependencies]
clap = { version = "*", features = ["derive"] }use clap::Parser;
#[derive(Parser)]
#[command(name = "myapp", version, about)]
struct Cli {
/// Input file
#[arg(short, long)]
input: PathBuf,
/// Verbosity level
#[arg(short, long, action = clap::ArgAction::Count)]
verbose: u8,
#[command(subcommand)]
command: Commands,
}
#[derive(clap::Subcommand)]
enum Commands {
/// Process files
Process { files: Vec<PathBuf> },
/// Show config
Config,
}argh
Google's lightweight alternative.
[dependencies]
argh = "*"Recommendation: Clap for full-featured CLI, argh for simplicity.
---
Error Handling
thiserror (Libraries)
Derive macros for custom error types.
[dependencies]
thiserror = "*"use thiserror::Error;
#[derive(Debug, Error)]
pub enum MyError {
#[error("not found: {0}")]
NotFound(String),
#[error("invalid input: {message}")]
Validation { message: String },
#[error("database error")]
Database(#[from] sqlx::Error),
#[error("io error")]
Io(#[from] std::io::Error),
}anyhow (Applications)
Convenient error handling for apps.
[dependencies]
anyhow = "*"use anyhow::{Context, Result, bail};
fn process_file(path: &Path) -> Result<Data> {
let content = std::fs::read_to_string(path)
.context("failed to read input file")?;
if content.is_empty() {
bail!("file is empty");
}
Ok(parse(content)?)
}eyre
anyhow alternative with better error reports.
[dependencies]
eyre = "*"
color-eyre = "*" # Pretty error reportsComparison
| Crate | Use Case | Features |
|---|---|---|
| thiserror | Libraries | Custom error types, From impls |
| anyhow | Applications | Easy error handling, context |
| eyre | Applications | Better reports, custom hooks |
| snafu | Large projects | Context selectors, backtraces |
Recommendation: thiserror for libs, anyhow for apps.
---
Serialization
Serde (Required)
The serialization framework for Rust.
[dependencies]
serde = { version = "*", features = ["derive"] }
serde_json = "*"use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct User {
id: i64,
full_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
email: Option<String>,
#[serde(default)]
active: bool,
}Format Crates
[dependencies]
serde_json = "*" # JSON
toml = "*" # TOML (config files)
serde_yaml = "*" # YAML
csv = "*" # CSV---
Logging & Tracing
tracing (Recommended)
Structured diagnostics, async-aware.
[dependencies]
tracing = "*"
tracing-subscriber = { version = "*", features = ["env-filter"] }use tracing::{info, warn, error, instrument, span, Level};
// Initialize
tracing_subscriber::fmt()
.with_env_filter("myapp=debug,tower_http=debug")
.init();
// Logging
info!(user_id = %id, "user logged in");
warn!(?error, "operation failed, retrying");
// Instrument functions
#[instrument(skip(db))]
async fn get_user(db: &PgPool, id: i64) -> Result<User> {
info!("fetching user");
// ...
}
// Manual spans
let span = span!(Level::INFO, "processing", batch_size = items.len());
let _guard = span.enter();log (Legacy)
Simple logging facade.
[dependencies]
log = "*"
env_logger = "*"Recommendation: tracing for new projects (async-aware, structured).
---
HTTP Client
reqwest (Recommended)
High-level async HTTP client.
[dependencies]
reqwest = { version = "*", features = ["json"] }let client = reqwest::Client::new();
let response = client
.post("https://api.example.com/users")
.json(&user)
.send()
.await?
.json::<ApiResponse>()
.await?;ureq
Blocking HTTP client, minimal dependencies.
[dependencies]
ureq = { version = "*", features = ["json"] }Recommendation: reqwest for async, ureq for sync/simple scripts.
---
Configuration
config-rs (Recommended)
Layered configuration from multiple sources.
[dependencies]
config = "*"use config::{Config, Environment, File};
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct Settings {
server: ServerConfig,
database: DatabaseConfig,
}
fn load_config() -> Result<Settings, config::ConfigError> {
Config::builder()
.add_source(File::with_name("config/default"))
.add_source(File::with_name("config/local").required(false))
.add_source(Environment::with_prefix("APP").separator("__"))
.build()?
.try_deserialize()
}figment
Alternative with better composition.
[dependencies]
figment = { version = "*", features = ["toml", "env"] }---
Testing
Built-in + tokio-test
[dev-dependencies]
tokio-test = "*"#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sync() {
assert_eq!(add(2, 2), 4);
}
#[tokio::test]
async fn test_async() {
let result = async_operation().await;
assert!(result.is_ok());
}
}Test Utilities
[dev-dependencies]
pretty_assertions = "*" # Better diff output
test-case = "*" # Parameterized tests
mockall = "*" # Mocking
fake = "*" # Fake data generationuse test_case::test_case;
#[test_case(0, 0, 0)]
#[test_case(1, 1, 2)]
#[test_case(2, 2, 4)]
fn test_add(a: i32, b: i32, expected: i32) {
assert_eq!(add(a, b), expected);
}---
Complete Stack Example
# Cargo.toml for web service
[dependencies]
# Async
tokio = { version = "*", features = ["full"] }
# Web
axum = "*"
tower = "*"
tower-http = { version = "*", features = ["cors", "trace"] }
# Database
sqlx = { version = "*", features = ["runtime-tokio", "tls-native-tls", "postgres", "macros", "migrate"] }
# Serialization
serde = { version = "*", features = ["derive"] }
serde_json = "*"
# Error handling
thiserror = "*"
anyhow = "*"
# Observability
tracing = "*"
tracing-subscriber = { version = "*", features = ["env-filter"] }
# Config
config = "*"
# Utils
uuid = { version = "*", features = ["v4", "serde"] }
chrono = { version = "*", features = ["serde"] }
[dev-dependencies]
tokio-test = "*"[package]
name = "mycli"
version = "0.1.0"
edition = "2024"
[dependencies]
# CLI parsing
clap = { version = "*", features = ["derive"] }
# Error handling
anyhow = "*"
thiserror = "*"
# Serialization
serde = { version = "*", features = ["derive"] }
serde_json = "*"
# Observability
tracing = "*"
tracing-subscriber = { version = "*", features = ["env-filter"] }
# Utilities
chrono = "*"
mod process;
use anyhow::Result;
use clap::Subcommand;
use std::path::Path;
#[derive(Subcommand)]
pub enum Commands {
/// Process input files
Process {
/// Input files to process
#[arg(required = true)]
files: Vec<std::path::PathBuf>,
/// Output directory
#[arg(short, long, default_value = "output")]
output: std::path::PathBuf,
/// Output format
#[arg(short, long, default_value = "json")]
format: OutputFormat,
},
/// Show configuration
Config,
/// Initialize a new project
Init {
/// Project name
#[arg(default_value = ".")]
path: std::path::PathBuf,
},
}
#[derive(Clone, clap::ValueEnum)]
pub enum OutputFormat {
Json,
Yaml,
Text,
}
impl Commands {
pub fn execute(&self, config_path: &Path) -> Result<()> {
match self {
Commands::Process { files, output, format } => {
process::run(files, output, format)
}
Commands::Config => {
println!("Config path: {}", config_path.display());
Ok(())
}
Commands::Init { path } => {
println!("Initializing project at: {}", path.display());
Ok(())
}
}
}
}
use anyhow::{Context, Result};
use std::path::Path;
use tracing::{info, warn};
use super::OutputFormat;
pub fn run(files: &[std::path::PathBuf], output: &Path, format: &OutputFormat) -> Result<()> {
info!("Processing {} files", files.len());
// Create output directory if it doesn't exist
std::fs::create_dir_all(output)
.with_context(|| format!("failed to create output directory: {}", output.display()))?;
for file in files {
if !file.exists() {
warn!("File not found: {}", file.display());
continue;
}
info!("Processing: {}", file.display());
process_file(file, output, format)?;
}
info!("Done!");
Ok(())
}
fn process_file(input: &Path, output: &Path, format: &OutputFormat) -> Result<()> {
let content = std::fs::read_to_string(input)
.with_context(|| format!("failed to read: {}", input.display()))?;
let output_name = input
.file_stem()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_else(|| "output".into());
let extension = match format {
OutputFormat::Json => "json",
OutputFormat::Yaml => "yaml",
OutputFormat::Text => "txt",
};
let output_path = output.join(format!("{}.{}", output_name, extension));
// Process content (placeholder)
let processed = content.to_uppercase();
std::fs::write(&output_path, processed)
.with_context(|| format!("failed to write: {}", output_path.display()))?;
info!("Wrote: {}", output_path.display());
Ok(())
}
use thiserror::Error;
#[derive(Debug, Error)]
pub enum CliError {
#[error("file not found: {0}")]
FileNotFound(String),
#[error("invalid format: {0}")]
InvalidFormat(String),
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("parse error: {0}")]
Parse(String),
}
use anyhow::Result;
use clap::Parser;
use std::path::PathBuf;
mod commands;
mod error;
use commands::Commands;
#[derive(Parser)]
#[command(name = "mycli")]
#[command(version, about = "A CLI tool", long_about = None)]
struct Cli {
/// Verbose output
#[arg(short, long, action = clap::ArgAction::Count)]
verbose: u8,
/// Config file path
#[arg(short, long, default_value = "config.toml")]
config: PathBuf,
#[command(subcommand)]
command: Commands,
}
fn main() -> Result<()> {
let cli = Cli::parse();
// Initialize tracing based on verbosity
let filter = match cli.verbose {
0 => "warn",
1 => "info",
2 => "debug",
_ => "trace",
};
tracing_subscriber::fmt()
.with_env_filter(filter)
.init();
// Execute command
cli.command.execute(&cli.config)?;
Ok(())
}
# Application configuration
APP_LISTEN_ADDR=127.0.0.1:3000
APP_DATABASE_URL=postgres://postgres:password@localhost:5432/myapp
APP_LOG_LEVEL=debug
# For SQLx compile-time checking
DATABASE_URL=postgres://postgres:password@localhost:5432/myapp
[package]
name = "myapp"
version = "0.1.0"
edition = "2024"
[dependencies]
# Async runtime
tokio = { version = "*", features = ["full"] }
# Web framework
axum = "*"
tower = "*"
tower-http = { version = "*", features = ["cors", "trace"] }
# Database
sqlx = { version = "*", features = ["runtime-tokio", "tls-native-tls", "postgres", "macros", "migrate"] }
# Serialization
serde = { version = "*", features = ["derive"] }
serde_json = "*"
# Error handling
thiserror = "*"
anyhow = "*"
# Observability
tracing = "*"
tracing-subscriber = { version = "*", features = ["env-filter"] }
# Configuration
config = "*"
# Utilities
uuid = { version = "*", features = ["v4", "serde"] }
chrono = { version = "*", features = ["serde"] }
[dev-dependencies]
tokio-test = "*"
[lib]
name = "myapp"
path = "src/lib.rs"
[[bin]]
name = "myapp"
path = "src/main.rs"
# Application Configuration
# Environment variables override these values with APP_ prefix
# e.g., APP_DATABASE_URL=postgres://...
listen_addr = "127.0.0.1:3000"
database_url = "postgres://localhost/myapp"
log_level = "debug"
.PHONY: build run dev test lint fmt check clean db-create db-migrate db-prepare
APP_NAME = myapp
# Build release binary
build:
cargo build --release
# Run in development mode
run:
cargo run
# Run with auto-reload (requires cargo-watch)
dev:
cargo watch -x run
# Run tests
test:
cargo test
# Run tests with coverage (requires cargo-tarpaulin)
test-coverage:
cargo tarpaulin --out Html
# Run clippy linter
lint:
cargo clippy -- -D warnings
# Format code
fmt:
cargo fmt
# Check formatting without modifying
fmt-check:
cargo fmt --check
# Run all checks
check: fmt lint test
@echo "All checks passed!"
# Clean build artifacts
clean:
cargo clean
# Update dependencies
upgrade:
cargo update
# Database commands (requires sqlx-cli)
db-create:
sqlx database create
db-drop:
sqlx database drop
db-migrate:
sqlx migrate run
db-revert:
sqlx migrate revert
# Prepare SQLx offline mode
db-prepare:
cargo sqlx prepare
# Help
help:
@echo "Available commands:"
@echo " build - Build release binary"
@echo " run - Run in development mode"
@echo " dev - Run with auto-reload"
@echo " test - Run tests"
@echo " lint - Run clippy linter"
@echo " fmt - Format code"
@echo " check - Run all checks"
@echo " clean - Clean build artifacts"
@echo " db-create - Create database"
@echo " db-migrate - Run migrations"
@echo " db-prepare - Prepare SQLx offline mode"
-- Initial migration: Create users table
CREATE TABLE IF NOT EXISTS users (
id UUID PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
name VARCHAR(255) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_users_email ON users(email);
use config::{Config, Environment, File};
use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub struct AppConfig {
pub listen_addr: String,
pub database_url: String,
pub log_level: String,
}
impl Default for AppConfig {
fn default() -> Self {
Self {
listen_addr: "127.0.0.1:3000".into(),
database_url: "postgres://localhost/myapp".into(),
log_level: "debug".into(),
}
}
}
pub fn load() -> anyhow::Result<AppConfig> {
let config = Config::builder()
// Start with defaults
.set_default("listen_addr", "127.0.0.1:3000")?
.set_default("log_level", "debug")?
// Load from config file if exists
.add_source(File::with_name("config/default").required(false))
.add_source(File::with_name("config/local").required(false))
// Override with environment variables (APP_ prefix)
.add_source(
Environment::with_prefix("APP")
.separator("__")
.try_parsing(true),
)
.build()?;
Ok(config.try_deserialize()?)
}
use sqlx::postgres::{PgPool, PgPoolOptions};
use std::time::Duration;
pub async fn connect(database_url: &str) -> anyhow::Result<PgPool> {
let pool = PgPoolOptions::new()
.max_connections(10)
.acquire_timeout(Duration::from_secs(5))
.connect(database_url)
.await?;
tracing::info!("connected to database");
Ok(pool)
}
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
use serde_json::json;
pub type Result<T> = std::result::Result<T, AppError>;
#[derive(Debug, thiserror::Error)]
pub enum AppError {
#[error("not found: {0}")]
NotFound(String),
#[error("validation error: {0}")]
Validation(String),
#[error("unauthorized")]
Unauthorized,
#[error("forbidden")]
Forbidden,
#[error("conflict: {0}")]
Conflict(String),
#[error("internal error")]
Internal(#[from] anyhow::Error),
#[error("database error")]
Database(#[from] sqlx::Error),
}
impl AppError {
pub fn not_found(resource: impl Into<String>) -> Self {
Self::NotFound(resource.into())
}
pub fn validation(msg: impl Into<String>) -> Self {
Self::Validation(msg.into())
}
pub fn conflict(msg: impl Into<String>) -> Self {
Self::Conflict(msg.into())
}
}
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::Forbidden => (StatusCode::FORBIDDEN, "forbidden".into()),
AppError::Conflict(msg) => (StatusCode::CONFLICT, msg.clone()),
AppError::Internal(err) => {
tracing::error!(?err, "internal error");
(StatusCode::INTERNAL_SERVER_ERROR, "internal error".into())
}
AppError::Database(err) => {
tracing::error!(?err, "database error");
(StatusCode::INTERNAL_SERVER_ERROR, "internal error".into())
}
};
let body = json!({
"error": {
"message": message,
"code": status.as_u16()
}
});
(status, Json(body)).into_response()
}
}
/// Extension trait for Option to convert to NotFound error
pub trait OptionExt<T> {
fn or_not_found(self, resource: &str) -> Result<T>;
}
impl<T> OptionExt<T> for Option<T> {
fn or_not_found(self, resource: &str) -> Result<T> {
self.ok_or_else(|| AppError::not_found(resource))
}
}
use axum::Json;
use serde_json::{json, Value};
pub async fn check() -> Json<Value> {
Json(json!({
"status": "ok"
}))
}
pub mod health;
pub mod user;
use axum::{
extract::{Path, State},
http::StatusCode,
Json,
};
use std::sync::Arc;
use uuid::Uuid;
use crate::error::Result;
use crate::models::{CreateUserInput, UpdateUserInput, User};
use crate::services::user as user_service;
use crate::AppState;
pub async fn get(
State(state): State<Arc<AppState>>,
Path(id): Path<Uuid>,
) -> Result<Json<User>> {
let user = user_service::find_by_id(&state.db, id).await?;
Ok(Json(user))
}
pub async fn create(
State(state): State<Arc<AppState>>,
Json(input): Json<CreateUserInput>,
) -> Result<(StatusCode, Json<User>)> {
let user = user_service::create(&state.db, input).await?;
Ok((StatusCode::CREATED, Json(user)))
}
pub async fn update(
State(state): State<Arc<AppState>>,
Path(id): Path<Uuid>,
Json(input): Json<UpdateUserInput>,
) -> Result<Json<User>> {
let user = user_service::update(&state.db, id, input).await?;
Ok(Json(user))
}
pub async fn delete(
State(state): State<Arc<AppState>>,
Path(id): Path<Uuid>,
) -> Result<StatusCode> {
user_service::delete(&state.db, id).await?;
Ok(StatusCode::NO_CONTENT)
}
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 use error::{AppError, Result};
/// Application state shared across handlers
pub struct AppState {
pub db: sqlx::PgPool,
}
impl AppState {
pub fn new(db: sqlx::PgPool) -> Arc<Self> {
Arc::new(Self { db })
}
}
use anyhow::Result;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
#[tokio::main]
async fn main() -> Result<()> {
// Initialize tracing
tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "myapp=debug,tower_http=debug".into()),
)
.with(tracing_subscriber::fmt::layer())
.init();
// Load configuration
let config = myapp::config::load()?;
// Connect to database
let db = myapp::db::connect(&config.database_url).await?;
// Run migrations
sqlx::migrate!().run(&db).await?;
// Build application state
let state = myapp::AppState::new(db);
// Build router
let app = myapp::router::build(state);
// Start server
let listener = tokio::net::TcpListener::bind(&config.listen_addr).await?;
tracing::info!("listening on {}", config.listen_addr);
axum::serve(listener, app).await?;
Ok(())
}
mod user;
pub use user::*;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
pub struct User {
pub id: Uuid,
pub email: String,
pub name: String,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Deserialize)]
pub struct CreateUserInput {
pub email: String,
pub name: String,
}
#[derive(Debug, Deserialize)]
pub struct UpdateUserInput {
pub email: Option<String>,
pub name: Option<String>,
}
use axum::{
routing::{get, post},
Router,
};
use std::sync::Arc;
use tower_http::trace::TraceLayer;
use crate::{handlers, AppState};
pub fn build(state: Arc<AppState>) -> Router {
Router::new()
// Health check
.route("/health", get(handlers::health::check))
// API v1
.nest("/api/v1", api_v1())
// Middleware
.layer(TraceLayer::new_for_http())
// State
.with_state(state)
}
fn api_v1() -> Router<Arc<AppState>> {
Router::new()
// Users
.route("/users", post(handlers::user::create))
.route("/users/:id", get(handlers::user::get))
.route("/users/:id", axum::routing::put(handlers::user::update))
.route("/users/:id", axum::routing::delete(handlers::user::delete))
}
pub mod user;
use sqlx::PgPool;
use uuid::Uuid;
use crate::error::{AppError, OptionExt, Result};
use crate::models::{CreateUserInput, UpdateUserInput, User};
pub async fn find_by_id(db: &PgPool, id: Uuid) -> Result<User> {
sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", id)
.fetch_optional(db)
.await?
.or_not_found("user")
}
pub async fn find_by_email(db: &PgPool, email: &str) -> Result<Option<User>> {
let user = sqlx::query_as!(User, "SELECT * FROM users WHERE email = $1", email)
.fetch_optional(db)
.await?;
Ok(user)
}
pub async fn create(db: &PgPool, input: CreateUserInput) -> Result<User> {
// Check if email already exists
if find_by_email(db, &input.email).await?.is_some() {
return Err(AppError::conflict("email already exists"));
}
let user = sqlx::query_as!(
User,
r#"
INSERT INTO users (id, email, name, created_at, updated_at)
VALUES ($1, $2, $3, NOW(), NOW())
RETURNING *
"#,
Uuid::new_v4(),
input.email,
input.name
)
.fetch_one(db)
.await?;
Ok(user)
}
pub async fn update(db: &PgPool, id: Uuid, input: UpdateUserInput) -> Result<User> {
// Check user exists
find_by_id(db, id).await?;
// Check email uniqueness if updating email
if let Some(ref email) = input.email {
if let Some(existing) = find_by_email(db, email).await? {
if existing.id != id {
return Err(AppError::conflict("email already exists"));
}
}
}
let user = sqlx::query_as!(
User,
r#"
UPDATE users
SET email = COALESCE($2, email),
name = COALESCE($3, name),
updated_at = NOW()
WHERE id = $1
RETURNING *
"#,
id,
input.email,
input.name
)
.fetch_one(db)
.await?;
Ok(user)
}
pub async fn delete(db: &PgPool, id: Uuid) -> Result<()> {
let result = sqlx::query!("DELETE FROM users WHERE id = $1", id)
.execute(db)
.await?;
if result.rows_affected() == 0 {
return Err(AppError::not_found("user"));
}
Ok(())
}