Course outline

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.

About 5 min readContent reviewed
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.

rust A public type with private representation
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());
}
Expected output
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.

rust Import one public operation by path
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)); }
Expected output
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.

Official references