10. Modules and Crates
📋 Jump to Takeaways🎁 Imagine a 10,000-line main.rs where every function, struct, and constant lives in one scrollable nightmare. Rust's module system lets you split code into logical pieces — with clear boundaries, explicit visibility, and zero header-file chaos. And when you're ready, you can publish those pieces to crates.io for the world to use.
The mod Keyword — Inline Modules
The simplest module is inline — a named block inside the same file.
mod math {
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
fn secret() -> i32 {
42 // private — only accessible inside `math`
}
}
fn main() {
println!("{}", math::add(2, 3)); // 5
// math::secret(); // ERROR: function `secret` is private
}Everything inside a module is private by default. You opt into visibility with pub.
pub Visibility
pub controls what's accessible from outside the module. You can make functions, structs, fields, and even entire modules public.
mod user {
pub struct User {
pub name: String,
email: String, // private field
}
impl User {
pub fn new(name: &str, email: &str) -> User {
User {
name: name.to_string(),
email: email.to_string(),
}
}
pub fn email(&self) -> &str {
&self.email
}
}
}
fn main() {
let u = user::User::new("Alice", "[email protected]");
println!("{}", u.name); // "Alice" — public field
// println!("{}", u.email); // ERROR: field `email` is private
println!("{}", u.email()); // "[email protected]" — via public method
}This gives you encapsulation without classes or access modifiers lists — just pub or not.
use Statements
Typing full paths gets tedious. The use keyword brings items into scope.
mod geometry {
pub mod shapes {
pub fn circle_area(r: f64) -> f64 {
f64::consts::PI * r * r
}
}
}
use geometry::shapes::circle_area;
fn main() {
println!("{:.2}", circle_area(3.0)); // 28.27
}You can also rename imports to avoid conflicts:
use std::fmt::Result as FmtResult;
use std::io::Result as IoResult;File-Based Modules
In real projects, each module lives in its own file. When you write mod math; the compiler looks for math.rs (or math/mod.rs).
src/
├── main.rs
├── math.rs
└── network/
├── mod.rs
└── tcp.rs// src/main.rs
mod math;
mod network;
fn main() {
println!("{}", math::add(1, 2)); // 3
network::tcp::connect();
}// src/math.rs
pub fn add(a: i32, b: i32) -> i32 {
a + b
}// src/network/mod.rs
pub mod tcp;// src/network/tcp.rs
pub fn connect() {
println!("Connected!"); // "Connected!"
}The directory name matches the module name, and mod.rs declares its sub-modules.
File Structure Conventions
Rust supports two conventions for nested modules:
Old style (mod.rs):
src/network/mod.rs → mod network
src/network/tcp.rs → mod tcp (inside network)New style (Rust 2018+):
src/network.rs → mod network
src/network/tcp.rs → mod tcp (inside network)Both work. The new style avoids dozens of files named mod.rs. Most new projects use the 2018+ convention.
The Crate Root: lib.rs vs main.rs
Every Rust package has a crate root:
src/main.rs— binary crate (produces an executable)src/lib.rs— library crate (produces reusable code)
You can have both in one package.
// src/lib.rs — your library's public API
pub mod auth;
pub mod database;
pub fn version() -> &'static str {
"1.0.0"
}// src/main.rs — uses the library
use my_app::version;
fn main() {
println!("App version: {}", version()); // "App version: 1.0.0"
}The binary crate refers to the library crate by the package name in Cargo.toml.
External Crates and Cargo.toml
To use code from crates.io, add it to Cargo.toml:
[package]
name = "my_app"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1", features = ["full"] }
anyhow = "1.0"
clap = { version = "4", features = ["derive"] }Then use them in your code:
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug)]
struct Config {
host: String,
port: u16,
}
fn main() {
let config = Config { host: "localhost".into(), port: 8080 };
let json = serde_json::to_string(&config).unwrap();
println!("{}", json); // {"host":"localhost","port":8080}
}Cargo downloads, compiles, and links dependencies automatically.
Common Crates to Know
These are the "standard library extensions" most Rust developers reach for:
| Crate | Purpose |
|---|---|
serde |
Serialization/deserialization (JSON, TOML, YAML) |
tokio |
Async runtime (networking, timers, I/O) |
anyhow |
Ergonomic error handling for applications |
thiserror |
Derive Error for library error types |
clap |
Command-line argument parsing |
reqwest |
HTTP client |
tracing |
Structured logging and diagnostics |
rand |
Random number generation |
You don't need to memorize these — just know they exist so you don't reinvent wheels.
Re-exporting with pub use
Sometimes your internal module structure doesn't match the API you want to expose. pub use re-exports items at a different path.
// src/lib.rs
mod internal {
pub mod validators {
pub fn is_valid_email(s: &str) -> bool {
s.contains('@')
}
}
}
// Re-export so users write `my_crate::is_valid_email` instead of
// `my_crate::internal::validators::is_valid_email`
pub use internal::validators::is_valid_email;// Consumer code
use my_crate::is_valid_email;
fn main() {
println!("{}", is_valid_email("[email protected]")); // true
}This keeps your public API clean while your internals stay organized however makes sense.
Workspaces
When your project grows into multiple crates, a workspace ties them together. They share a single target/ directory and Cargo.lock.
# Cargo.toml (workspace root)
[workspace]
members = [
"core",
"api",
"cli",
]my_project/
├── Cargo.toml (workspace)
├── core/
│ ├── Cargo.toml
│ └── src/lib.rs
├── api/
│ ├── Cargo.toml
│ └── src/main.rs
└── cli/
├── Cargo.toml
└── src/main.rsEach member is an independent crate. They can depend on each other:
# api/Cargo.toml
[dependencies]
core = { path = "../core" }Workspaces keep compile times fast and dependencies consistent across related crates.
Key Takeaways
moddeclares modules — inline or file-based- Everything is private by default; use
pubto expose items usebrings paths into scope to avoid repetition- File-based modules map to
filename.rsorfilename/mod.rs src/main.rsis the binary root;src/lib.rsis the library root- External crates go in
Cargo.tomlunder[dependencies] pub usere-exports items to create a clean public API- Workspaces manage multi-crate projects with shared builds
🎁 Your code is organized, your crates are published, but there's more power hiding in plain sight. Next up: Closures — anonymous functions that capture variables from their environment. Rust's ownership rules make them zero-cost abstractions, and they're the foundation of iterators, threads, and async code.