Skip to editor content
learningrust.orglesson 17 of 26

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 main function and their root file is src/main.rs.
  • Library crates — compile to a reusable .rlib that other crates can depend on. Their root file is src/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 pub to expose them to the outside
  • For structs, both the struct itself and each field need separate pub declarations
  • The use keyword 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 statement
  • super navigates to the parent module; self refers 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.toml and downloaded automatically by Cargo

Pro Tip: When designing a library, start with everything private and only add pub when 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