| name | rust-programming-expert |
| description | Expert skill for Rust programming (Rust 2024 / v1.85+). Use whenever the user writes, reviews, or debugs Rust code — covering memory safety (ownership, lifetimes), async (Tokio, async closures), web backends (Axum, SQLx), CLI tools (Clap, Serde), unsafe code, performance optimization, and Cargo profiling. Trigger on any mention of Rust, Cargo, Tokio, Axum, SQLx, Clap, Serde, lifetimes, ownership, or Rust error handling. Also trigger for systems programming, WebAssembly, or high-performance service development in Rust.
|
Rust Programming Expert (2024 Edition / v1.85+)
Production Rust patterns: memory safety, async-first, web backends, CLI, and performance.
Project Setup
cargo new my-app --bin
cargo new my-lib --lib
[package]
name = "my-app"
version = "0.1.0"
edition = "2024"
[dependencies]
tokio = { version = "1", features = ["full"] }
axum = "0.8"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sqlx = { version = "0.8", features = ["postgres", "runtime-tokio", "uuid", "chrono"] }
anyhow = "1"
thiserror = "2"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
uuid = { version = "1", features = ["v4", "serde"] }
chrono = { version = "0.4", features = ["serde"] }
[dev-dependencies]
tokio-test = "0.4"
Ownership & Borrowing Patterns
Common Ownership Patterns
fn process(data: Vec<String>) -> Vec<String> {
let upper: Vec<String> = data.iter().map(|s| s.to_uppercase()).collect();
upper
}
fn count_long(items: &[String], min_len: usize) -> usize {
items.iter().filter(|s| s.len() >= min_len).count()
}
use std::borrow::Cow;
fn sanitize(input: &str) -> Cow<str> {
if input.contains('<') {
Cow::Owned(input.replace('<', "<"))
} else {
Cow::Borrowed(input)
}
}
Lifetime Annotations
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
struct Parser<'a> {
input: &'a str,
pos: usize,
}
impl<'a> Parser<'a> {
fn new(input: &'a str) -> Self {
Self { input, pos: 0 }
}
fn remaining(&self) -> &'a str {
&self.input[self.pos..]
}
}
Error Handling
thiserror for Library Errors
use thiserror::Error;
#[derive(Error, Debug)]
pub enum AppError {
#[error("Not found: {0}")]
NotFound(String),
#[error("Unauthorized")]
Unauthorized,
#[error("Validation failed: {field} — {message}")]
Validation { field: String, message: String },
#[error("Database error")]
Database(#[from] sqlx::Error),
#[error("IO error")]
Io(#[from] std::io::Error),
}
pub type Result<T> = std::result::Result<T, AppError>;
anyhow for Application Code
use anyhow::{Context, Result, bail, ensure};
async fn load_config(path: &str) -> Result<Config> {
let content = tokio::fs::read_to_string(path)
.await
.with_context(|| format!("Failed to read config file: {path}"))?;
let config: Config = serde_json::from_str(&content)
.context("Config file is not valid JSON")?;
ensure!(config.port > 1024, "Port must be > 1024, got {}", config.port);
Ok(config)
}
Axum Error Response
use axum::{http::StatusCode, response::{IntoResponse, Response}, Json};
use serde_json::json;
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let (status, message) = match &self {
AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg.clone()),
AppError::Unauthorized => (StatusCode::UNAUTHORIZED, "Unauthorized".into()),
AppError::Validation { field, message } => (
StatusCode::UNPROCESSABLE_ENTITY,
format!("{field}: {message}"),
),
AppError::Database(_) | AppError::Io(_) => {
tracing::error!(error = ?self, "Internal error");
(StatusCode::INTERNAL_SERVER_ERROR, "Internal server error".into())
}
};
(status, Json(json!({ "error": message }))).into_response()
}
}
Async with Tokio
Main Entry Point
#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.init();
let config = load_config("config.json").await?;
let app = build_app(config).await?;
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
tracing::info!("Listening on {}", listener.local_addr()?);
axum::serve(listener, app).await?;
Ok(())
}
Concurrent Tasks
use tokio::task::JoinSet;
async fn fetch_all(ids: Vec<String>) -> Vec<Result<Data>> {
let mut set = JoinSet::new();
for id in ids {
set.spawn(async move { fetch_one(&id).await });
}
let mut results = Vec::new();
while let Some(res) = set.join_next().await {
results.push(res.expect("task panicked"));
}
results
}
async fn fetch_dashboard(user_id: &str) -> Result<Dashboard> {
let (user, posts, stats) = tokio::try_join!(
fetch_user(user_id),
fetch_posts(user_id),
fetch_stats(user_id),
)?;
Ok(Dashboard { user, posts, stats })
}
Axum Web Backend
App Setup with State
use axum::{Router, extract::State, routing::{get, post}};
use std::sync::Arc;
#[derive(Clone)]
pub struct AppState {
pub db: sqlx::PgPool,
pub config: Arc<Config>,
}
pub fn build_router(state: AppState) -> Router {
Router::new()
.route("/health", get(health_check))
.nest("/api/v1", api_routes())
.with_state(state)
.layer(
tower::ServiceBuilder::new()
.layer(tower_http::trace::TraceLayer::new_for_http())
.layer(tower_http::cors::CorsLayer::permissive())
)
}
fn api_routes() -> Router<AppState> {
Router::new()
.route("/posts", get(list_posts).post(create_post))
.route("/posts/:id", get(get_post).delete(delete_post))
}
Handlers
use axum::{
extract::{Path, Query, State, Json},
http::StatusCode,
};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Deserialize)]
pub struct ListQuery {
page: Option<u32>,
limit: Option<u32>,
}
#[derive(Serialize)]
pub struct PostResponse {
id: Uuid,
title: String,
content: Option<String>,
}
pub async fn list_posts(
State(state): State<AppState>,
Query(query): Query<ListQuery>,
) -> Result<Json<Vec<PostResponse>>, AppError> {
let page = query.page.unwrap_or(1).max(1);
let limit = query.limit.unwrap_or(10).min(100);
let offset = (page - 1) * limit;
let posts = sqlx::query_as!(
PostResponse,
"SELECT id, title, content FROM posts WHERE published = true
ORDER BY created_at DESC LIMIT $1 OFFSET $2",
limit as i64,
offset as i64,
)
.fetch_all(&state.db)
.await?;
Ok(Json(posts))
}
#[derive(Deserialize)]
pub struct CreatePost {
title: String,
content: Option<String>,
}
pub async fn create_post(
State(state): State<AppState>,
Json(body): Json<CreatePost>,
) -> Result<(StatusCode, Json<PostResponse>), AppError> {
if body.title.trim().is_empty() {
return Err(AppError::Validation {
field: "title".into(),
message: "must not be empty".into(),
});
}
let post = sqlx::query_as!(
PostResponse,
"INSERT INTO posts (title, content) VALUES ($1, $2) RETURNING id, title, content",
body.title,
body.content,
)
.fetch_one(&state.db)
.await?;
Ok((StatusCode::CREATED, Json(post)))
}
CLI with Clap
use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(name = "myapp", about = "My CLI tool", version)]
pub struct Cli {
#[arg(short, long, default_value = "info")]
pub log_level: String,
#[command(subcommand)]
pub command: Command,
}
#[derive(Subcommand)]
pub enum Command {
Serve {
#[arg(short, long, default_value = "3000")]
port: u16,
},
Migrate,
GenKey,
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
match cli.command {
Command::Serve { port } => serve(port).await?,
Command::Migrate => migrate().await?,
Command::GenKey => println!("{}", generate_key()),
}
Ok(())
}
Performance Tips
let mut vec = Vec::with_capacity(items.len());
fn is_valid(s: &str) -> bool { }
fn process(items: &[Item]) -> usize { items.len() }
use rayon::prelude::*;
let sum: u64 = big_vec.par_iter().map(|x| expensive(x)).sum();
use bytes::Bytes;
async fn handler() -> Bytes {
Bytes::from_static(b"hello")
}
Key Rules
- Prefer
Result<T, E> over unwrap() — only unwrap() in tests or truly infallible
thiserror for libraries, anyhow for binaries
Arc<T> for shared state, Mutex<T> only when mutation needed
tokio::spawn for fire-and-forget, JoinSet for tracked tasks
sqlx::query_as! macro — compile-time query validation
- Never block in async context — use
tokio::task::spawn_blocking for CPU work
tracing not println! — structured logging always
- Clippy is not optional —
cargo clippy -- -D warnings in CI
#[derive(Clone)] on state — Axum requires Clone on shared state
- Edition 2024 in
Cargo.toml — use latest language features