Modules, Crates & Visibility

Rust · Lesson 18 of 31 7 min read Updated

A 50-line program fits happily in main.rs. A 5,000-line program does not. As code grows you need to split it into named parts, decide which parts other code may use, and hide the details that might change. Rust's module system does this.

In this lesson you will learn the difference between packages, crates and modules, create modules inline and in separate files, control visibility with pub, and refer to items with paths and use.

Packages, crates and modules

  • A package is what cargo new creates: a folder with a Cargo.toml. It contains one or more crates.
  • A crate is one unit the compiler builds: a binary crate (a program, rooted at src/main.rs) or a library crate (code for other crates to use, rooted at src/lib.rs). A package can have at most one library crate and any number of binaries.
  • A module is a named section inside a crate. Modules can contain functions, structs, enums, constants and other modules, forming a tree whose root is the crate itself.

Everything in a module is private by default: only code in the same module (and its child modules) can use it. Adding pub makes an item visible to the parent module and beyond.

Modules and pub

You can write a module directly in a file with mod name { ... }:

Rust
mod billing {
    pub struct Invoice {
        pub customer: String,
        amount_paise: u64,          // private field
    }

    impl Invoice {
        pub fn new(customer: &str, amount_paise: u64) -> Self {
            Self { customer: customer.to_string(), amount_paise }
        }

        pub fn total_with_gst(&self) -> u64 {
            self.amount_paise + gst::amount(self.amount_paise)
        }
    }

    mod gst {                       // private module: only `billing` can use it
        pub fn amount(paise: u64) -> u64 {
            paise * 18 / 100
        }
    }
}

use billing::Invoice;

fn main() {
    let bill = Invoice::new("Isha Traders", 250_000);
    println!("{} owes {} paise", bill.customer, bill.total_with_gst());
    // println!("{}", bill.amount_paise);   // error: field `amount_paise` is private
    // billing::gst::amount(100);           // error: module `gst` is private
}
Output
Isha Traders owes 295000 paise

Notice the details:

  • A pub struct can still have private fields. Code outside billing cannot read or set amount_paise directly, and cannot even create an Invoice with Invoice { ... }; it must call Invoice::new. This is how you protect rules such as "the amount is never changed after creation".
  • Methods need their own pub. A private method is a helper for the module itself.
  • For a pub enum, all variants are automatically public, because an enum with hidden variants could not be matched.
  • gst is private to billing, but billing's own code can use it.

Paths and use

A path names an item in the module tree. Paths starting with crate:: are absolute (from the crate root); others are relative to the current module. super:: means the parent module, like .. in a folder path.

Rust
const SHOP_NAME: &str = "Kabir Electronics";

mod store {
    pub mod pricing {
        pub fn with_discount(price: u32, percent: u32) -> u32 {
            price - price * percent / 100
        }
    }

    pub mod display {
        pub fn label(item: &str, price: u32) -> String {
            let sale = super::pricing::with_discount(price, 10);
            format!("{}: {item} at Rs. {sale}", crate::SHOP_NAME)
        }
    }
}

use store::display::label;

fn main() {
    println!("{}", label("Earphones", 1_499));
    println!("{}", store::pricing::with_discount(800, 25));
}
Output
Kabir Electronics: Earphones at Rs. 1350
600

use creates a shortcut so you can write label instead of store::display::label. Some conventions and forms you will see everywhere:

Rust
use std::collections::{BTreeMap, HashSet};   // several items from one path
use std::io::{self, Write};                  // the module itself plus an item
use std::fmt::Result as FmtResult;           // rename to avoid a clash

By convention, functions are imported through their parent module (use std::fs; then fs::read_to_string(...)), so readers can see where they come from, while structs, enums and traits are imported by name (use std::collections::HashMap;).

Visibility levels

Written asVisible to
(nothing)The current module and its children
pub(super)The parent module
pub(crate)The whole crate, but not other crates
pubEveryone, including other crates that depend on yours

pub(crate) is very useful in libraries: helpers shared between your own modules, without becoming part of the public API that you must keep stable.

Modules in separate files

Real projects put each module in its own file. mod billing; (with a semicolon instead of a body) tells the compiler to load the module from src/billing.rs. A child module gst of billing lives in src/billing/gst.rs:

Text
marks_report/
├── Cargo.toml
└── src/
    ├── main.rs          mod billing;
    ├── billing.rs       mod gst;  pub struct Invoice { ... }
    └── billing/
        └── gst.rs       pub fn amount(...) { ... }

The contents of each file are exactly what was between the curly brackets of the inline module. Declaring mod once, in the parent, is all that is needed; there is no #include, and Rust never picks up a file you did not declare.

A library and a binary in one package

Putting your logic in src/lib.rs and keeping src/main.rs small is a common pattern: the library can be tested on its own and reused by other programs. Inside main.rs, the library is used by the package name:

Rust
// src/lib.rs
pub mod grades {
    pub fn grade(marks: u32) -> char {
        if marks >= 75 {
            'A'
        } else if marks >= 50 {
            'B'
        } else {
            'C'
        }
    }
}

pub use grades::grade;   // re-export: users can write marks_report::grade
Rust
// src/main.rs
use marks_report::grade;

fn main() {
    for marks in [82, 64, 41] {
        println!("{marks} -> {}", grade(marks));
    }
}
Output
82 -> A
64 -> B
41 -> C

pub use re-exports an item, so the public path can be shorter than the internal file layout. External crates work the same way: after cargo add serde, you write use serde::Serialize;. A package name with a hyphen, such as marks-report, is written with an underscore in code: marks_report.

Common mistakes

  • Creating a file but not declaring it. src/billing.rs is ignored until a parent says mod billing;.
  • Declaring the same module twice. Write mod billing; once (in main.rs or lib.rs), then reach it everywhere with crate::billing or use.
  • Making a struct pub and forgetting its fields. Fields stay private; add pub to fields or provide a constructor and methods.
  • Making everything pub. Every public item is a promise to other code. Start private, then open up with pub(crate) or pub when needed.

Key points

  • A package has a Cargo.toml and contains crates; a crate is a tree of modules rooted at main.rs or lib.rs.
  • Items are private by default; pub, pub(crate) and pub(super) widen access. Struct fields need their own pub.
  • Paths use crate:: (absolute), super:: (parent) and self::; use brings names into scope.
  • mod name; loads name.rs (or name/mod.rs in older layouts); child modules go in a folder with the parent's name.
  • Put logic in lib.rs, keep main.rs thin, and re-export public items with pub use.

Exercise

Create a module tree for a school app in one file: a module school with a public child attendance (a public function percentage(present: u32, total: u32) -> u32) and a private child rules (a function is_eligible(percent: u32) -> bool that returns true at 75 or above). Add a public function school::exam_status(present, total) -> String that uses both, and call only that function from main.

Show solution
Rust
mod school {
    pub mod attendance {
        pub fn percentage(present: u32, total: u32) -> u32 {
            present * 100 / total
        }
    }

    mod rules {
        pub fn is_eligible(percent: u32) -> bool {
            percent >= 75
        }
    }

    pub fn exam_status(present: u32, total: u32) -> String {
        let p = attendance::percentage(present, total);
        if rules::is_eligible(p) {
            format!("{p}% attendance: may sit the exam")
        } else {
            format!("{p}% attendance: not eligible")
        }
    }
}

fn main() {
    println!("{}", school::exam_status(170, 200));
    println!("{}", school::exam_status(130, 200));
}
Output
85% attendance: may sit the exam
65% attendance: not eligible

rules is private to school, so main cannot call school::rules::is_eligible directly, but exam_status, which lives inside school, can. Its own function still needs pub so that the parent module school can call it.

Quick Quiz

1. What is the default visibility of a function inside a module?

2. Where does mod billing; in src/main.rs look for the module's code?

3. You have pub struct Invoice { amount: u64 } in a module. What can code outside the module do?

4. What does pub(crate) mean?

Learn Rust with a mentor

Free lessons get you started. Our Programming & Software Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call