Essential Crates
Rust's standard library is deliberately small. The ecosystem fills the gaps with crates that have become de facto standards. If you are building a production service in Rust, you will use most of these. Knowing them saves weeks of evaluation and experimentation.
Serde: Serialization & Deserialization
Serde is the serialization framework for Rust. Almost every crate that deals with data formats uses it.
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
struct Config {
name: String,
port: u16,
debug: bool,
tags: Vec<String>,
}
fn main() {
let config = Config {
name: "my-service".to_string(),
port: 8080,
debug: false,
tags: vec!["production".to_string()],
};
// To JSON
let json = serde_json::to_string_pretty(&config).unwrap();
println!("{}", json);
// From JSON
let parsed: Config = serde_json::from_str(&json).unwrap();
println!("{:?}", parsed);
}
{
"name": "my-service",
"port": 8080,
"debug": false,
"tags": [
"production"
]
}
Serde works with JSON, TOML, YAML, MessagePack, CBOR, and dozens of other formats. The same #[derive(Serialize, Deserialize)] works everywhere. Attribute macros handle field renaming, defaults, and custom logic:
#[derive(Serialize, Deserialize)]
struct User {
#[serde(rename = "user_name")]
name: String,
#[serde(default)]
active: bool,
#[serde(skip_serializing_if = "Option::is_none")]
email: Option<String>,
}
Tokio: The Async Runtime
Tokio is the async runtime for Rust. It provides the executor, I/O primitives, timers, and channels that async code needs to run.
use tokio::time::{sleep, Duration};
use tokio::fs;
#[tokio::main]
async fn main() {
// Spawn concurrent tasks
let handle1 = tokio::spawn(async {
sleep(Duration::from_millis(100)).await;
"task 1 done"
});
let handle2 = tokio::spawn(async {
let content = fs::read_to_string("data.txt").await.unwrap();
content.len()
});
let result1 = handle1.await.unwrap();
let result2 = handle2.await.unwrap();
println!("{}, file length: {}", result1, result2);
}
Tokio powers Axum, SQLx, reqwest, tonic (gRPC), and most async Rust libraries. If you are doing async Rust, you are using Tokio.
Key features:
- Multi-threaded executor — work-stealing scheduler for CPU-bound tasks
- I/O reactor — efficient async file, network, and timer operations
- Channels —
mpsc,oneshot,broadcast,watchfor task communication - Synchronization —
Mutex,RwLock,Semaphorethat work across await points
Reqwest: HTTP Client
Reqwest is the go-to HTTP client. Built on Tokio and hyper.
use serde::Deserialize;
#[derive(Deserialize, Debug)]
struct ApiResponse {
id: u64,
title: String,
}
#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
// Simple GET
let body = reqwest::get("https://httpbin.org/get")
.await?
.text()
.await?;
println!("{}", body);
// JSON response
let posts: Vec<ApiResponse> = reqwest::Client::new()
.get("https://jsonplaceholder.typicode.com/posts")
.query(&[("_limit", "3")])
.send()
.await?
.json()
.await?;
for post in &posts {
println!("{}: {}", post.id, post.title);
}
// POST with JSON body
let client = reqwest::Client::new();
let response = client
.post("https://httpbin.org/post")
.json(&serde_json::json!({
"name": "Alice",
"email": "alice@example.com"
}))
.header("Authorization", "Bearer my-token")
.send()
.await?;
println!("Status: {}", response.status());
Ok(())
}
Reqwest handles connection pooling, TLS, redirects, cookies, and timeouts. Create a Client once and reuse it — it maintains a connection pool internally.
Tracing: Structured Logging
Tracing replaces println! debugging with structured, leveled, span-aware logging.
use tracing::{info, warn, error, instrument};
#[instrument(skip(pool))]
async fn create_user(pool: &PgPool, name: &str, email: &str) -> Result<User, Error> {
info!(user_name = name, user_email = email, "Creating new user");
let user = db::insert_user(pool, name, email).await.map_err(|e| {
error!(error = %e, "Failed to insert user");
e
})?;
info!(user_id = user.id, "User created successfully");
Ok(user)
}
fn main() {
// Initialize the subscriber
tracing_subscriber::fmt()
.with_env_filter("my_app=debug,tower_http=info")
.json() // JSON output for production
.init();
}
{"timestamp":"2024-01-15T10:30:00Z","level":"INFO","target":"my_app","message":"Creating new user","user_name":"alice","user_email":"alice@example.com"}
#[instrument] automatically creates a span for the function, including its arguments. Structured fields are searchable in log aggregation systems. Use tracing over log — it is the modern standard.
Clap: Command Line Parsing
Clap builds CLI argument parsers with derive macros:
use clap::Parser;
#[derive(Parser)]
#[command(name = "myapp", about = "A sample CLI application")]
struct Args {
/// Input file to process
#[arg(short, long)]
input: String,
/// Output file path
#[arg(short, long, default_value = "output.txt")]
output: String,
/// Verbosity level
#[arg(short, long, action = clap::ArgAction::Count)]
verbose: u8,
}
fn main() {
let args = Args::parse();
println!("Input: {}, Output: {}, Verbosity: {}",
args.input, args.output, args.verbose);
}
myapp --help
A sample CLI application
Usage: myapp [OPTIONS] --input <INPUT>
Options:
-i, --input <INPUT> Input file to process
-o, --output <OUTPUT> Output file path [default: output.txt]
-v, --verbose... Verbosity level
-h, --help Print help
Clap generates help text, validates arguments, handles subcommands, and produces shell completions. The derive API covers most use cases; the builder API is available for complex scenarios.
SQLx & Axum
Covered in their own topics, but worth mentioning here as part of the production stack:
- SQLx — async database toolkit with compile-time query checking. Supports PostgreSQL, MySQL, and SQLite.
- Axum — web framework built on Tower and hyper. Type-safe routing, middleware, and extractors.
Together with Tokio, Serde, and Tracing, these form the complete stack for building production web services in Rust.
How to Evaluate Crates
Not every crate on crates.io is worth using. Evaluate based on:
Downloads and recent activity. Check crates.io for download counts and the date of the last release. A crate with millions of downloads and recent updates is well-maintained.
Documentation quality. Good crates have comprehensive docs on docs.rs. If the documentation is sparse, maintenance is likely sparse too.
Dependencies. Check the dependency tree with cargo tree. A crate that pulls in 200 transitive dependencies for a simple task is a liability.
Maintainer reputation. Is it maintained by a well-known Rust developer or team? Is it used by other popular crates?
API stability. Check the version number. Pre-1.0 crates may change their API. Post-1.0 crates commit to semantic versioning.
# Check dependency tree
cargo tree
# Check for outdated dependencies
cargo outdated
# Check for duplicated dependencies
cargo tree --duplicates
The Production Crate Stack
A typical production Rust service uses:
// Cargo.toml
[dependencies]
# Web
axum = "0.7"
tower-http = { version = "0.5", features = ["trace", "cors", "compression"] }
# Async runtime
tokio = { version = "1", features = ["full"] }
# Serialization
serde = { version = "1", features = ["derive"] }
serde_json = "1"
# Database
sqlx = { version = "0.7", features = ["runtime-tokio", "postgres"] }
# HTTP client
reqwest = { version = "0.12", features = ["json"] }
# Logging
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
# Configuration
dotenvy = "0.15"
# Error handling
thiserror = "1"
anyhow = "1"
This stack handles web serving, database access, HTTP clients, serialization, logging, and error handling. It is what most production Rust services look like.
Common Pitfalls
- Reinventing what crates solve. Do not write your own JSON parser or HTTP client. The ecosystem crates are battle-tested and maintained.
- Adding crates for trivial functionality. Not everything needs a dependency. A three-line function does not need a crate.
- Not pinning versions in production. Use exact versions or lockfiles to ensure reproducible builds.
Cargo.lockshould be committed for applications (not libraries). - Ignoring feature flags. Many crates use feature flags to control what gets compiled.
tokio = { features = ["full"] }is convenient for development but pulls in everything. For production, enable only what you need. - Using unmaintained crates. Check the last commit date and open issues. A crate abandoned for two years is a future migration headache.
Key Takeaways
- Serde is the universal serialization framework. Learn it once, use it with every data format.
- Tokio is the async runtime. If you are doing async Rust, you are using Tokio.
- Reqwest, Tracing, and Clap are the standards for HTTP clients, logging, and CLI parsing.
- Evaluate crates on downloads, documentation, dependencies, and maintenance activity.
- The production stack is Axum + Tokio + SQLx + Serde + Tracing. These six crates form the foundation of most Rust services.