Modules in Rust
Organize code with mod, pub, use, crate structure, and Cargo workspaces.
Modules in a Single File
Modules are Rust’s namespace mechanism. They let you group related functions, structs, and types together, control what is visible to the outside world, and avoid name collisions between unrelated parts of a codebase. Items inside a module are private by default — you opt into visibility with pub rather than opting out of it.
// src/main.rs
mod math {
// Private by default — only callable within this module
fn gcd(mut a: u64, mut b: u64) -> u64 {
while b != 0 { let t = b; b = a % b; a = t; }
a
}
// pub — accessible from outside the module
pub fn lcm(a: u64, b: u64) -> u64 {
a / gcd(a, b) * b // can call private gcd here since we're in the same module
}
// Modules can be nested to any depth
pub mod stats {
pub fn mean(data: &[f64]) -> f64 {
data.iter().sum::<f64>() / data.len() as f64
}
pub fn variance(data: &[f64]) -> f64 {
let m = mean(data);
data.iter().map(|x| (x - m).powi(2)).sum::<f64>() / data.len() as f64
}
}
}
use math::stats; // bring stats into scope to avoid typing math::stats every time
fn main() {
println!("{}", math::lcm(12, 8)); // 24
println!("{:.2}", stats::mean(&[1.0, 2.0, 3.0, 4.0, 5.0])); // 3.00
println!("{:.2}", stats::variance(&[2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0])); // 4.00
}
Separating Modules into Files
As a project grows, keeping all code in one file becomes unwieldy. Rust lets you move each module into its own file. The mod geometry; declaration in main.rs tells the compiler to look for the module’s content in src/geometry.rs, and sub-modules follow the same pattern recursively.
src/
├── main.rs
├── geometry.rs
└── geometry/
├── shapes.rs
└── transforms.rs
src/main.rs
mod geometry; // compiler loads src/geometry.rs
use geometry::shapes::Circle;
fn main() {
let c = Circle::new(5.0);
println!("area: {:.2}", c.area());
}
src/geometry.rs
// Declares sub-modules; compiler loads src/geometry/shapes.rs etc.
pub mod shapes;
pub mod transforms;
src/geometry/shapes.rs
use std::f64::consts::PI;
pub struct Circle {
pub radius: f64,
}
impl Circle {
pub fn new(radius: f64) -> Self { Self { radius } }
pub fn area(&self) -> f64 { PI * self.radius * self.radius }
pub fn circumference(&self) -> f64 { 2.0 * PI * self.radius }
}
pub struct Rectangle {
pub width: f64,
pub height: f64,
}
impl Rectangle {
pub fn new(width: f64, height: f64) -> Self { Self { width, height } }
pub fn area(&self) -> f64 { self.width * self.height }
}
Visibility Modifiers
Rust’s visibility system is fine-grained. Rather than just public or private, you can restrict visibility to a specific module boundary. This lets you share implementation details between sibling modules in a crate without exposing them to users of the crate’s public API.
pub struct Config {
pub name: String, // visible to everyone — part of the public API
pub(crate) version: u32, // visible anywhere within this crate only
pub(super) timeout: u64, // visible in the parent module only
secret_key: String, // private — visible only within this module
}
mod inner {
pub fn public_fn() {}
fn private_fn() {} // callable only from within `inner`
pub(super) fn super_fn() {} // callable from the parent module
}
The use Keyword
use brings items into scope so you can refer to them by their short name. Without it, you would need to write the full path every time. use also enables re-exporting — marking an item pub use makes it part of your module’s public API even though it is defined elsewhere, letting you design a clean public surface that is independent of your internal file structure.
use std::collections::HashMap;
use std::fmt::{self, Display, Formatter}; // self brings fmt itself, Display and Formatter separately
use std::io::{self, Read, Write};
// Glob import — convenient but risks name collisions; use sparingly
use std::collections::*;
// Alias — useful when two items have the same short name
use std::collections::HashMap as Map;
// Re-export — Circle becomes part of this module's public API
pub use crate::geometry::shapes::Circle;
Paths — Absolute vs Relative
Rust paths work like filesystem paths. crate:: is the root of the current crate (like /). super:: goes up one module level (like ../). Relative paths are shorter but can break if you move code; absolute paths are more explicit. Both are valid — prefer whichever is clearer in context.
mod outer {
pub fn foo() {}
pub mod inner {
pub fn bar() {
crate::outer::foo(); // absolute path from crate root
super::foo(); // relative path — go up one level to outer, then foo
}
}
}
fn main() {
crate::outer::foo(); // absolute
crate::outer::inner::bar();
outer::foo(); // relative (from crate root in main.rs)
outer::inner::bar();
use outer::inner::bar; // bring into scope for shorter call syntax
bar();
}
Library Crate (lib.rs)
A crate with src/lib.rs is a library. Its public API is whatever is pub at the top level of lib.rs. Re-exporting the most important types at the top level with pub use makes your library ergonomic to use — consumers can write use my_lib::Parser instead of use my_lib::parser::inner::Parser.
// src/lib.rs
pub mod parser;
pub mod renderer;
// Flatten the public API — consumers don't need to know the internal module structure
pub use parser::Parser;
pub use renderer::{Renderer, RenderOptions};
/// Library version from Cargo.toml — available at compile time
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
Users of the crate:
use my_lib::{Parser, Renderer}; // top-level re-exports — clean and simple
use my_lib::renderer::RenderOptions; // or import from the module directly
Cargo.toml and Dependencies
Cargo.toml is the manifest for your crate. It declares metadata, dependencies, features, and the location of binary entry points. Understanding its structure lets you build libraries, multiple binaries, and feature-gated code all within one package.
[package]
name = "my_project"
version = "0.1.0"
edition = "2021"
# Multiple binary targets in one package
[[bin]]
name = "server"
path = "src/bin/server.rs"
[[bin]]
name = "cli"
path = "src/bin/cli.rs"
[lib]
name = "my_project"
path = "src/lib.rs"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
[dev-dependencies]
# Only compiled when running tests or examples
tempfile = "3"
[features]
default = ["tls"]
tls = ["dep:rustls"]
Cargo Workspaces
Workspaces let multiple related crates share a single Cargo.lock file and build cache. This is the standard structure for larger projects — a core library crate, a server binary, and a CLI binary all share dependencies and are built and tested together. Changes to the core library automatically trigger rebuilds of the crates that depend on it.
workspace/
├── Cargo.toml # workspace manifest — lists all members
├── core/ # shared library crate
│ ├── Cargo.toml
│ └── src/lib.rs
├── server/ # binary crate — depends on core
│ ├── Cargo.toml
│ └── src/main.rs
└── cli/ # binary crate — depends on core
├── Cargo.toml
└── src/main.rs
workspace/Cargo.toml
[workspace]
members = ["core", "server", "cli"]
resolver = "2"
# Declare shared dependency versions once — member crates inherit them
[workspace.dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
workspace/server/Cargo.toml
[package]
name = "server"
version = "0.1.0"
edition = "2021"
[dependencies]
core = { path = "../core" }
serde.workspace = true # inherit version from workspace — no duplication
tokio.workspace = true
Build and test everything at once:
cargo build --workspace
cargo test --workspace
cargo run -p server
The prelude Pattern
Large libraries like Tokio and Rayon expose a prelude module containing the most commonly needed items. Users can do a single wildcard import to get everything they need without listing dozens of individual imports. If you are writing a library with many frequently used types, the prelude pattern is worth adopting.
// src/lib.rs
pub mod prelude {
// Re-export the items most users will need — a curated convenience bundle
pub use crate::{Parser, Renderer, RenderOptions, Config};
pub use crate::error::{Error, Result};
}
Users can then write:
use my_lib::prelude::*; // one line brings in everything commonly needed
This pattern is used by Tokio (use tokio::prelude::*), Rayon, and many others.