11 - Crates and Cargo¶
What this session is¶
About 45 minutes. You'll learn how Rust code is organized - modules (folders/files), crates (one library or binary, like a "project"), and crates.io (the public package registry). You'll add a third-party dependency and use it.
Vocabulary¶
- Package - what
cargo newcreates: a folder withCargo.tomland one or more crates inside. - Crate - either a library (compiles to
.rlib/.so) or a binary (compiles to an executable). A package usually has one crate. - Module - a unit of organization inside a crate. Maps to files and folders.
For most beginners, one package = one crate. We'll keep it that way here.
Single-file projects: organization with mod¶
For a single binary file, you can declare modules inline:
mod math {
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
pub fn subtract(a: i32, b: i32) -> i32 {
a - b
}
}
fn main() {
println!("{}", math::add(2, 3));
println!("{}", math::subtract(10, 4));
}
What's new:
- mod math { ... } - declare a module called math.
- pub fn - make the function public (visible outside the module). Without pub, it'd be private to the module.
- math::add(2, 3) - call a function inside the module with ::.
Multi-file modules¶
Once a module gets big, move it to its own file:
In main.rs:
mod math; // declares that there's a module called math; Cargo finds it in math.rs
fn main() {
println!("{}", math::add(2, 3));
}
In math.rs:
mod math; in main.rs is the declaration. Cargo looks for either math.rs or math/mod.rs. Either works.
For multi-level modules:
src/
├── main.rs
└── math/
├── mod.rs (or src/math.rs with src/math/ for submodules)
├── basic.rs
└── advanced.rs
In main.rs: mod math;. In math/mod.rs: pub mod basic; pub mod advanced;.
Library vs binary projects¶
If your project has src/main.rs, Cargo builds a binary. If it has src/lib.rs, Cargo builds a library. You can have both (a binary that uses the library) by having both files.
For a library you intend to publish or share, the convention is src/lib.rs with everything organized into modules under it.
use for imports¶
To avoid typing full paths everywhere, use use:
use std::collections::HashMap;
fn main() {
let mut h = HashMap::new(); // shorter than std::collections::HashMap::new
h.insert("alice", 30);
}
use std::collections::HashMap; brings HashMap into scope. You can use it unqualified within the file.
Multiple imports from the same path:
use std::collections::{HashMap, HashSet, BTreeMap};
use std::io::{self, Read, Write}; // self brings io itself in too
Glob (rarely used):
use is conventional for everything outside the current file. For your own modules:
crates.io: the public registry¶
crates.io hosts the Rust community's open-source libraries. Hundreds of thousands of crates: HTTP clients, serializers, web frameworks, CLI parsers, database drivers, everything.
To use a crate, add it to Cargo.toml:
Run cargo build (or cargo check) and Cargo downloads the crate + all its dependencies, builds them, and links them.
Convention: pin the major version. serde = "1.0" accepts 1.0.x and 1.y.z (semver-compatible).
A real example using serde and serde_json for JSON:
The features = ["derive"] enables an optional feature of the crate (here, the derive macros for serde).
In src/main.rs:
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug)]
struct Person {
name: String,
age: u32,
}
fn main() {
let alice = Person { name: String::from("Alice"), age: 30 };
let json = serde_json::to_string(&alice).unwrap();
println!("{}", json); // {"name":"Alice","age":30}
let parsed: Person = serde_json::from_str(&json).unwrap();
println!("{:?}", parsed); // Person { name: "Alice", age: 30 }
}
#[derive(Serialize, Deserialize)] makes the struct JSON-convertible. Magic - but at compile time. No runtime reflection.
Run with cargo run.
Useful crates to know exist¶
For recognition only - you'll meet these in real code:
| Crate | What it does |
|---|---|
serde, serde_json, serde_yaml |
Serialization |
tokio |
Async runtime |
reqwest |
HTTP client |
axum, actix-web |
Web frameworks |
clap |
Command-line argument parsing |
anyhow, thiserror |
Error handling helpers |
tracing |
Structured logging |
criterion |
Benchmarking |
rayon |
Easy parallelism |
sqlx, diesel |
Database drivers/ORMs |
rstest |
Parameterized tests |
These are the "standard" crates almost every Rust project uses.
Cargo.toml fields¶
[package]
name = "my-app"
version = "0.1.0"
edition = "2024"
authors = ["You <you@example.com>"]
description = "A short description"
license = "MIT OR Apache-2.0"
repository = "https://github.com/you/my-app"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
[dev-dependencies]
rstest = "0.23"
[dev-dependencies] - only used for tests, examples, benchmarks. Not in the final binary.
Cargo.lock¶
After cargo build, you'll see a Cargo.lock file. It records exact versions of every dependency (direct + transitive) used for the build.
- For binary projects (apps): commit
Cargo.lock. Ensures reproducible builds across machines. - For library projects (published crates): don't commit
Cargo.lock. Downstream users will have their own.
Useful Cargo commands recap¶
| Command | What it does |
|---|---|
cargo new --bin <name> |
New binary project. |
cargo new --lib <name> |
New library project. |
cargo add <crate> |
Add a dependency (modifies Cargo.toml). |
cargo remove <crate> |
Remove a dependency. |
cargo update |
Update dependencies (Cargo.lock). |
cargo build / cargo build --release |
Compile. |
cargo run |
Build and run. |
cargo check |
Type-check without building. |
cargo test |
Run tests. |
cargo doc --open |
Generate and open docs. |
cargo clippy |
Run the linter. |
cargo fmt |
Auto-format. |
cargo publish |
Publish to crates.io (needs an account). |
Exercise¶
In your project:
-
Add
(Or editserde(withfeatures = ["derive"]) andserde_json:Cargo.tomldirectly.) -
Define a struct
Bookwithtitle: Stringandauthor: Stringandyear: u32. DeriveSerialize,Deserialize,Debug. -
Create a
Book, serialize to JSON, print it. -
Parse the JSON back, print the result.
-
Now do it for a
Vec<Book>of 3 books. -
Stretch: add
clap(cargo add clap --features derive). Make a small CLI that takes a--nameflag and printsHello, <name>. Look up "clap derive" examples.
What you might wonder¶
"Why are there so many crates instead of a big standard library?" Philosophy. Rust's standard library is intentionally small. Most things (HTTP, async, serialization) live in third-party crates. Pro: stdlib stays small, evolves slowly, won't ship buggy features. Con: you have to pick crates for everything.
"How do I pick a crate?"
Check crates.io for stars, downloads, last update. Check the README. Check open issues on GitHub. For well-known categories: serde (JSON), tokio (async), reqwest (HTTP), clap (CLI) are de facto standards.
"Why are some imports serde:: and some std::?"
std is the standard library - always available. serde is a third-party crate you added. Cargo treats them the same syntactically.
"What's a workspace?"
A multi-package project. The top-level Cargo.toml lists member packages (subdirectories), each with their own Cargo.toml. Common in big projects. Not needed for one-package learning.
Done¶
- Organize code into modules with
mod. - Use
pubto control visibility. - Import with
use. - Add dependencies from crates.io via
Cargo.tomlorcargo add. - Recognize the major crates of the Rust ecosystem.
Next: Reading other people's code →