Rust / Intermediate / Lesson 10
Modules, crates, and documentation
Organize a Rust package around controlled visibility, explicit dependencies, and examples that can be checked as part of the public contract.
On this page
Lesson goals
- Distinguish module paths, crate boundaries, and package targets.
- Use privacy to preserve invariants behind constructors.
- Document ownership, errors, and supported compiler assumptions.
A module is a namespace and privacy boundary
Modules organize items within a crate; they are not automatically separate dependencies or compilation targets. A package manifest can define a library crate and one or more binaries, while dependencies normally refer to other packages and expose their crate names. mod declares a module, and use brings a path into scope without changing the underlying ownership or visibility. crate, self, and super identify path relationships. Understand the tree before moving files, because a filesystem directory alone does not make items public or select a binary target.
Expose operations instead of representation
Rust items are private by default, and pub exposes an item subject to the visibility of its containing path. A public struct does not automatically make its fields public. The first example keeps Counter.value private inside a module while exposing construction and observation methods. Callers cannot directly assign the field from outside that module. This creates a place to add validation later without rewriting every caller. Privacy does not replace tests, but it narrows the code permitted to bypass an invariant. Keep visibility no broader than the consumers actually require.
mod counters {
pub struct Counter { value: u32 }
impl Counter {
pub fn new(value: u32) -> Self { Self { value } }
pub fn value(&self) -> u32 { self.value }
}
}
fn main() {
let counter = counters::Counter::new(5);
println!("{}", counter.value());
}
5
The caller uses public methods to create and inspect the value. The private field is accessed only within its module.
Make imported names understandable
A use declaration shortens a path and can rename an imported item to resolve ambiguity. A pub use can re-export an interface so consumers need not know internal module layout. Re-exports create a public compatibility commitment: moving the implementation later should not casually break that path. The second example imports one function from an inline module and calls it directly. Inline modules make the demonstration self-contained; real packages can place modules in separate source files using the documented file conventions. Do not include implementation files by copying their text into multiple targets as a substitute for package design.
mod arithmetic {
pub fn batches(count: u32, width: u32) -> Option<u32> {
if width == 0 { return None; }
Some(count / width + u32::from(count % width != 0))
}
}
use arithmetic::batches;
fn main() { println!("{:?}", batches(9, 4)); }
Some(3)
Nine items need three batches of width four. The function rejects zero width before division.
Dependencies are executable supply-chain choices
Record edition and rust-version explicitly, and distinguish normal, development, and build dependencies. A build dependency can execute through a build script during compilation; adding a crate is more than importing a name. For these lessons no external crates are required. Use Cargo.lock to preserve resolved versions according to the project workflow and cargo commands with --offline when the cache and toolchain already support the build. Features select optional capabilities and can change a dependency graph; document important feature combinations instead of assuming one local successful build represents every consumer configuration.
Documentation should explain obligations
Rust documentation comments can include examples checked by cargo test as documentation tests. State what inputs mean, which values are owned or borrowed, whether the operation can panic, and what errors mean. Unsafe interfaces additionally need an explicit Safety section describing caller obligations. Avoid presenting a dependency snippet as a complete runnable program. Public docs should show the intended import path and edition assumptions so examples do not accidentally rely on private implementation details. Treat a failing documentation example as evidence that the public interface or its explanation has drifted, not merely as a formatting inconvenience.
Common errors and fixes
A public struct field cannot be accessed externally.
- Why it happens
- The field remains private unless explicitly exposed.
- How to fix it
- Use an appropriate public method or deliberate field visibility.
- Prevent it next time
- Expose domain operations before raw fields.
Moving a file breaks an imported path.
- Why it happens
- Filesystem layout and the declared module tree diverged.
- How to fix it
- Update module declarations and preserve intended re-exports.
- Prevent it next time
- Test public paths from a separate consumer.
An offline build unexpectedly needs a download.
- Why it happens
- A dependency or selected toolchain is not cached.
- How to fix it
- Prepare approved dependencies and tools before offline verification.
- Prevent it next time
- Keep dependency and toolchain requirements explicit.
Check your understanding
1. A consumer needs to read Counter.value but must not bypass validation. Choose a public field or a getter and explain a future implementation change.
Show worked solution
Keep the field private and expose value(&self) -> u32. Later the counter can store a different validated representation while preserving the getter contract. A public mutable field would allow callers to bypass construction and mutation rules. Test the public observable behavior rather than asserting private field layout.
2. Turn batches into a documented library operation. Name three cases for a documentation example and one obligation the prose should state.
Show worked solution
Show zero items returning Some(0), nine with width four returning Some(3), and zero width returning None. State that the function rounds nonnegative counts upward into complete batches and never divides by zero. Use the library public import path in the documentation test so an accidental private-path dependency is detected.
Key takeaways
- Modules control names and privacy within crates.
- Public paths and dependencies are compatibility commitments.
- Document and test ownership, errors, and examples.