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 — with two exceptions: the associated items of a pub trait, and the variants of a pub enum, which are public along with the item that declares them.

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.

That per-field rule is the single most common surprise in the module system, and it bites at the call site rather than at the definition — the struct compiles perfectly, and the error arrives in whoever tries to read the field. The program below is a small catalog that does exactly that. Commit a hypothesis about which line rustc rejects before you change anything:

Debug

This should build two products and total their prices to 2050 cents. The module compiles, the constructor works, and main looks ordinary — but it does not build. Name the line rustc rejects and why, then fix it the way a library author should.

mod catalog {
  pub struct Product {
      pub name: String,
      price_cents: u32,
  }

  impl Product {
      pub fn new(name: &str, price_cents: u32) -> Self {
          Product { name: name.to_string(), price_cents }
      }
  }
}

fn main() {
  // Build a product and total up an order.
  let items = vec![
      catalog::Product::new("Mug", 850),
      catalog::Product::new("Poster", 1200),
  ];

  let total: u32 = items.iter().map(|p| p.price_cents).sum();

  assert_eq!(total, 2050, "expected the order to total 2050 cents, got {}", total);
  println!("order total: {} cents", total);
  for p in &items {
      println!("  {}", p.name);
  }
}

Expected output: order total: 2050 cents Mug Poster

The rejected line is the map closure, with error[E0616]: field price_cents of struct catalog::Product is private. pub on a struct makes the type visible, never its fields — each field needs its own pub, which is why p.name on the next line is fine and p.price_cents is not. Notice where the error lands: inside main, not inside the module. The definition is legal Rust; only an outside reader is refused. Two fixes exist and they are not equal. Adding pub price_cents: u32 costs one word but publishes the representation, so storing the price in dollars later becomes a breaking change. The better fix is the one the module system is nudging you toward: an accessor, pub fn price_cents(&self) -> u32, called as p.price_cents(). It is the same instinct that put Product::new there in the first place — callers never assemble the struct field by field, so the module stays free to change how it stores things.

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."

Those constants were pub, so nothing surprising happened. But what if the parent's item is not pub? "Private" sounds absolute, and it is not — privacy in Rust has a direction. The program below has a parent module with a completely private function and a public child that calls it. Decide whether it compiles, and what happens in each direction, before you run it:

Predict

store::secret_rate has no pub anywhere on it. The child module store::pricing calls it through super::. The parent store also calls down into the child's pub function. Predict whether this compiles and what it prints — and, separately, what would happen if main tried to call store::secret_rate() directly.

mod store {
  // Private to store. No pub anywhere on it.
  fn secret_rate() -> u32 {
      7
  }

  pub mod pricing {
      // Reaching UP into the parent's private item.
      pub fn quote(units: u32) -> u32 {
          units * super::secret_rate()
      }
  }

  pub fn parent_quote(units: u32) -> u32 {
      // Reaching DOWN into the child's public item.
      pricing::quote(units)
  }
}

fn main() {
  println!("child sees parent's private fn: {}", store::pricing::quote(3));
  println!("parent calls child's pub fn:     {}", store::parent_quote(3));
}

Both lines print 21, and a call to store::secret_rate() from main would fail with error[E0603]: function secret_rate is private. Privacy in Rust is relative to the module tree, not absolute. A private item is visible inside the module that defines it and inside every module nested within it, so a child may reach up with super:: into its parent's internals. Reaching down is different: a parent sees only what the child marked pub, and code outside the subtree sees only what is pub at every level of the path. That asymmetry is what makes submodules worth having — you can split a large module into pieces that share private helpers without any of those helpers escaping into the crate's public API.

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.

Before assembling all of this, pull one prerequisite back out of memory. Every module above hangs its behaviour off an impl block, and the constructor pattern those blocks use is not a module-system idea at all — you met it several lessons earlier:

Recall

Without scrolling up: in Structs & Enums you wrote both Type::new(...) and value.method(). The Circle::new and Item::new above are the first kind. What distinguishes an associated function from a method, and why does that distinction matter the moment a struct's fields go private?

An associated function takes no self and is called on the type with ::; a method takes self, &self or &mut self and is called on a value with a dot. Both live in impl blocks, and the receiver is the whole difference. It becomes load-bearing the moment a field goes private, because struct-literal syntax requires every field to be visible to the code writing it — so an outside caller simply cannot construct the value. A pub associated function declared inside the module can see the private fields, which makes it the only door in. That is why a struct with private fields nearly always ships a new, and why that constructor is the natural place to enforce invariants: everything has to come through it.

Try It Yourself

Reading about visibility is not the same as designing a boundary. This is a build task: a small program that reports its own pass/fail. You are given a library module laid out the way a real crate would be — a private helper it does not export, a pub struct with one public and one private field, and a child module that reaches up into its parent — with three things missing. Run it as-is and it fails immediately, naming the first check that did not pass. Implement each until every check passes and it prints All checks passed.

Every one of the three is a visibility decision rather than an algorithm, and each body is a line or two. Book::new must reach the module's private normalize, which only code inside library can do. copies() is the accessor that makes a private field readable without publishing it — exactly the fix the debug block above argued for. And shelf::total_copies sits in a child module, so it must go through that accessor too, because copies is private to library and shelf is inside it. The whole file is one crate with no Cargo.toml, which is why every mod is declared inline.

Build

Finish the build. Three items are stubbed out and the checks below them fail until each behaves. Run it as-is to see which check fails first, decide what that item needs, then implement all three until it prints 'All checks passed.' The checks run top to bottom, so the first failure you see is TODO 1 — implement it first, then work down.

// A single-file crate laid out the way a real one would be: a library module
// with a narrow public surface, a private helper it does not export, and a
// child module that reaches up into its parent.
//
// Three things are missing, and every one of them is a VISIBILITY decision
// rather than an algorithm. The bodies you need are one line each.

mod library {
  // PRIVATE to library on purpose: the outside world must not depend on it.
  // Code inside library — including its child modules — can still call it.
  fn normalize(title: &str) -> String {
      title.trim().to_lowercase()
  }

  pub struct Book {
      pub title: String,
      // Deliberately NOT pub: callers go through an accessor.
      copies: u32,
  }

  impl Book {
      // TODO 1: build a Book whose title has been run through the module's
      //   private normalize helper, keeping copies as given.
      pub fn new(title: &str, copies: u32) -> Self {
          let _ = (title, copies);
          Book {
              title: String::new(),
              copies: 0,
          }
      }

      // TODO 2: expose the private copies field to the outside world.
      //   This is the accessor that makes copies private-but-readable.
      pub fn copies(&self) -> u32 {
          0
      }
  }

  pub mod shelf {
      use super::Book;

      // TODO 3: sum copies() across every book.
      //   You are INSIDE library::shelf, so Book is in scope from the use above.
      pub fn total_copies(books: &[Book]) -> u32 {
          let _ = books;
          0
      }

      pub fn titles(books: &[Book]) -> Vec<String> {
          books.iter().map(|b| b.title.clone()).collect()
      }
  }
}

use library::shelf::{titles, total_copies};
use library::Book;

// --- Build checks: these must all pass. Do not edit below this line. ---
fn main() {
  let books = vec![
      Book::new("  The Rust Book  ", 3),
      Book::new("Systems Programming", 2),
  ];

  assert_eq!(
      books[0].title, "the rust book",
      "Book::new should run the title through the module's private normalize helper"
  );
  assert_eq!(
      books[0].copies(),
      3,
      "copies() should expose the private field's value"
  );
  assert_eq!(
      total_copies(&books),
      5,
      "shelf::total_copies should sum copies() across every book"
  );
  assert_eq!(
      titles(&books),
      vec![
          String::from("the rust book"),
          String::from("systems programming")
      ],
      "shelf::titles should return each normalized title in order"
  );

  println!("All checks passed.");
  println!("titles: {:?}", titles(&books));
  println!("total copies: {}", total_copies(&books));
}

Expected output: All checks passed. titles: ["the rust book", "systems programming"] total copies: 5

Once it passes, try two variations and predict each before running:

  1. Construct the struct from outside. In main, replace the first Book::new(...) with a struct literal, Book { title: String::from("the rust book"), copies: 3 }, and predict what the compiler says before running. It is error[E0451]: field copies of struct Book is private — struct-literal syntax needs every field visible to the code writing it, and main is outside library. This is the constructor earning its existence: it is not a stylistic convention, it is the only door in.
  2. Publish the field instead of the accessor. Add pub to copies and change total_copies to read b.copies directly, then predict whether the checks still pass. They do — and that is the point. The compiler is perfectly happy, but you have now made the representation part of your public API, so a later change from a u32 count to, say, a Vec of copy records breaks every caller. The accessor version survives that change untouched.

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