Modules and Crates
As your Rust programs grow, keeping all your code in a single file becomes unwieldy. Rust's module system gives you precise control over how you structure, organize, and expose your code. Paired with crates — Rust's unit of compilation and code sharing — the module system forms the backbone of every real-world Rust project.
What Is a Module?
A module is a named container for items like functions, structs, enums, constants, and even other modules. You define one with the mod keyword. Everything inside a module is private by default — only the module itself can see it.
mod greetings {
pub fn hello(name: &str) -> String {
format!("Hello, {}!", name)
}
pub fn goodbye(name: &str) -> String {
format!("Goodbye, {}!", name)
}
}
fn main() {
let msg = greetings::hello("Alice");
println!("{}", msg);
let farewell = greetings::goodbye("Bob");
println!("{}", farewell);
}
The :: operator navigates the module path. greetings::hello means "the hello function inside the greetings module". Without pub, calling those functions from outside the module would be a compile error.
Controlling Visibility with pub
By default, items inside a module are private — only accessible to code within the same module. The pub keyword makes an item public, exposing it to the outside world.
mod geometry {
pub struct Circle {
pub radius: f64,
}
impl Circle {
pub fn new(radius: f64) -> Self {
Circle { radius }
}
pub fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
// Private helper — only usable inside this module
fn circumference(&self) -> f64 {
2.0 * std::f64::consts::PI * self.radius
}
}
}
fn main() {
let c = geometry::Circle::new(5.0);
println!("Radius: {}", c.radius);
println!("Area: {:.2}", c.area());
// c.circumference() would be a compile error — it's private
}
Notice that for structs, each field also needs pub separately if you want it accessible outside the module. This gives you fine-grained control: a struct can be public while keeping some of its internals private, forcing callers to use constructor functions instead of building the struct directly.
Nested Modules and use
Modules can contain other modules, building a tree-shaped hierarchy. Typing full module paths like library::fiction::recommend every time gets tedious, so the use keyword creates a shortcut, bringing items into the current scope.
mod math {
pub mod stats {
pub fn mean(values: &[f64]) -> f64 {
let sum: f64 = values.iter().sum();
sum / values.len() as f64
}
pub fn max(values: &[f64]) -> f64 {
values.iter().cloned().fold(f64::NEG_INFINITY, f64::max)
}
pub fn min(values: &[f64]) -> f64 {
values.iter().cloned().fold(f64::INFINITY, f64::min)
}
}
}
use math::stats::{mean, max, min};
fn main() {
let data = vec![3.0, 7.0, 2.0, 9.0, 5.0];
println!("Mean: {:.1}", mean(&data));
println!("Max: {:.1}", max(&data));
println!("Min: {:.1}", min(&data));
}
You can bring in multiple items from the same path using {item1, item2} syntax. You can also write use math::stats::* to import everything from a module, though this is generally discouraged outside of test modules because it makes it unclear where names come from.
The super and self Keywords
Inside a module, self refers to the current module and super refers to the parent module. These let you write relative paths instead of always starting from the crate root.
mod config {
pub const MAX_CONNECTIONS: u32 = 100;
pub const TIMEOUT_SECS: u64 = 30;
pub mod defaults {
use super::{MAX_CONNECTIONS, TIMEOUT_SECS};
pub fn print_limits() {
println!("Max connections: {}", MAX_CONNECTIONS);
println!("Default timeout: {}s", TIMEOUT_SECS);
}
pub fn is_within_limit(connections: u32) -> bool {
connections <= MAX_CONNECTIONS
}
}
}
use config::defaults;
fn main() {
defaults::print_limits();
println!("50 connections OK? {}", defaults::is_within_limit(50));
println!("200 connections OK? {}", defaults::is_within_limit(200));
}
use super::MAX_CONNECTIONS inside defaults means "go up to the parent module (config) and bring MAX_CONNECTIONS into scope here."
What Is a Crate?
A crate is Rust's fundamental unit of compilation. Every Rust project you build is a crate. There are two kinds:
- Binary crates — compile to an executable. They have a
mainfunction and their root file issrc/main.rs. - Library crates — compile to a reusable
.rlibthat other crates can depend on. Their root file issrc/lib.rs.
When you run cargo new my_project, Cargo creates a binary crate. Running cargo new --lib my_library creates a library crate.
The entire Rust ecosystem lives on crates.io. Adding a dependency is as simple as editing your Cargo.toml:
[dependencies]
serde = { version = "1", features = ["derive"] }
rand = "0.8"
chrono = "0.4"
After editing Cargo.toml, running cargo build downloads and compiles the dependencies automatically. Cargo tracks exact versions in Cargo.lock so builds are reproducible across machines.
Try It Yourself
Build a small inventory system using modules to practice organizing code across a namespace boundary:
mod inventory {
#[derive(Debug)]
pub struct Item {
pub name: String,
pub quantity: u32,
pub price: f64,
}
impl Item {
pub fn new(name: &str, quantity: u32, price: f64) -> Self {
Item {
name: name.to_string(),
quantity,
price,
}
}
pub fn total_value(&self) -> f64 {
self.quantity as f64 * self.price
}
}
pub mod reports {
use super::Item;
pub fn print_summary(items: &[Item]) {
println!("=== Inventory Report ===");
for item in items {
println!(
"{}: {} units @ ${:.2} = ${:.2}",
item.name,
item.quantity,
item.price,
item.total_value()
);
}
let total: f64 = items.iter().map(|i| i.total_value()).sum();
println!("----------------------");
println!("Total value: ${:.2}", total);
}
pub fn low_stock<'a>(items: &'a [Item], threshold: u32) -> Vec<&'a Item> {
items.iter().filter(|i| i.quantity < threshold).collect()
}
}
}
use inventory::Item;
use inventory::reports::{print_summary, low_stock};
fn main() {
let items = vec![
Item::new("Apples", 50, 0.30),
Item::new("Bread", 12, 2.50),
Item::new("Milk", 8, 1.80),
Item::new("Eggs", 24, 3.20),
];
print_summary(&items);
println!("\n=== Low Stock (< 15 units) ===");
for item in low_stock(&items, 15) {
println!(" {} — only {} left!", item.name, item.quantity);
}
}
Try extending this: add a most_valuable function to reports that returns the item with the highest total_value().
Key Takeaways
- Modules (
mod) group related code into named namespaces, keeping your codebase organized as it grows - Items inside a module are private by default — use
pubto expose them to the outside - For structs, both the struct itself and each field need separate
pubdeclarations - The
usekeyword imports items into the current scope to reduce repetitive path typing use module::{item1, item2}lets you import multiple items from one path in a single statementsupernavigates to the parent module;selfrefers to the current module- A crate is Rust's compilation unit — binary crates produce executables, library crates produce reusable APIs
- External crates are declared in
Cargo.tomland downloaded automatically by Cargo
Pro Tip: When designing a library, start with everything private and only add
pubwhen you have a concrete reason. Public items are a commitment — once other code depends on them, changing their signature is a breaking change. A narrow, intentional public surface is far easier to maintain and evolve than one that exposed implementation details by accident.
Next Steps
Now that you can structure your code with modules, it's time to learn about concurrency — how Rust enables safe concurrent programming with threads, message passing, and shared state.
Next lesson
Concurrency
Learn safe concurrent programming in Rust with threads, message passing, and shared state
30 min