Skip to content
C Codeloom
Rust

Rust Modules and Visibility Explained

Understand how Rust organizes code with modules, files, and the pub keyword, plus the visibility rules that keep crates clean and maintainable.

·5 min read · By Codeloom
Intermediate 9 min read

What you'll learn

  • How modules map to files and folders
  • What pub, pub(crate), and pub(super) do
  • Why re-exports help API design
  • How paths and use statements resolve
  • Common visibility mistakes and fixes

Prerequisites

  • Basic Rust familiarity

What and Why

Rust’s module system is the backbone of every non-trivial crate. It controls how code is split across files, what names callers can see, and which items remain private implementation details. Unlike languages where every file is automatically importable, Rust requires you to declare modules explicitly with mod, and to mark anything public with pub. This explicitness is annoying for five minutes and a lifesaver forever after, because the compiler enforces a clear public surface.

Visibility in Rust is not just cosmetic. The compiler uses it to drive dead-code analysis, documentation generation, and semver discipline. If you ever want to refactor an internal helper without breaking downstream users, visibility is your tool.

Mental Model

Think of a Rust crate as a tree. The crate root (lib.rs or main.rs) is the trunk. Each mod foo; declaration creates a branch. Items inside a branch are private to that branch unless marked pub. A child can always see into its parents, but parents must request access to children.

There are four common visibility levels you’ll touch daily: private (the default), pub(super) (visible to the parent module), pub(crate) (visible anywhere in this crate), and pub (visible to external consumers). You’ll mostly use private and pub(crate) internally, and reserve bare pub for the deliberate API surface.

Hands-on Example

Let’s build a small billing crate with an internal calculator and a public invoice type.

// src/lib.rs
mod calculator;        // private module
pub mod invoice;       // exposed to users

pub use invoice::Invoice;  // re-export for ergonomics
// src/calculator.rs
pub(crate) fn total(items: &[u32]) -> u32 {
    items.iter().sum()
}

fn debug_log(value: u32) {
    println!("computed: {value}");
}
// src/invoice.rs
use crate::calculator;

pub struct Invoice {
    pub customer: String,
    items: Vec<u32>,   // private field
}

impl Invoice {
    pub fn new(customer: &str) -> Self {
        Self { customer: customer.into(), items: vec![] }
    }

    pub fn add(&mut self, amount: u32) {
        self.items.push(amount);
    }

    pub fn total(&self) -> u32 {
        calculator::total(&self.items)
    }
}

Notice three things. First, calculator is not pub, so external users cannot call billing::calculator::total. Second, total is pub(crate), which lets invoice.rs use it without exposing it. Third, the items field is private, even though Invoice itself is public; callers must go through add.

Crate visibility tree

Common Pitfalls

Forgetting mod declarations. Creating src/calculator.rs does nothing unless something declares mod calculator;. Rust doesn’t auto-discover files.

Confusing pub with pub(crate). A field marked pub inside a pub(crate) struct is still only crate-visible, because the struct itself bounds visibility. Don’t over-export.

Glob imports hiding origin. use crate::utils::* makes code shorter, but readers lose the trail. Prefer explicit imports except for prelude-style modules.

Re-export sprawl. Beginners sometimes pub use everything at the crate root. That works briefly, then becomes a tangle of duplicate paths. Curate re-exports: pick a single canonical path for each item.

mod inside functions. It compiles, but nested function-local modules are almost always a sign you wanted a helper function instead.

Practical Tips

Start every module file private and promote items as needed. It’s easier to add pub later than to retract it after users depend on it.

Use pub(crate) aggressively for internal helpers. It documents intent: “this is shared inside the crate, not part of our API.”

Mirror your folder structure in the module tree. If you have src/auth/login.rs, declare pub mod login; in src/auth/mod.rs (or src/auth.rs in the 2018+ style). Consistency makes navigation predictable.

Re-export your “headline” types at the crate root with pub use. Users prefer billing::Invoice to billing::invoice::Invoice.

When you write a library, run cargo doc --open and look at the generated docs. If something appears that shouldn’t, your visibility is too loose. If something is missing, you forgot a pub.

Treat the public API like a contract. Every pub item is a promise you’ll keep across minor versions, so be intentional about what crosses the boundary.

Wrap-up

Rust’s module system feels heavy at first but pays off as projects grow. By learning the difference between pub, pub(crate), and pub(super), and by treating re-exports as deliberate API design rather than a convenience, you’ll keep crates approachable for newcomers and safe to refactor. The module tree is not bureaucracy; it’s a map of intent that the compiler enforces for you.