Modules, Crates & Visibility
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 newcreates: a folder with aCargo.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 atsrc/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 { ... }:
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
}Isha Traders owes 295000 paise
Notice the details:
- A
pub structcan still have private fields. Code outsidebillingcannot read or setamount_paisedirectly, and cannot even create anInvoicewithInvoice { ... }; it must callInvoice::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. gstis private tobilling, butbilling'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.
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));
}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:
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 clashBy 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 as | Visible to |
|---|---|
| (nothing) | The current module and its children |
pub(super) | The parent module |
pub(crate) | The whole crate, but not other crates |
pub | Everyone, 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:
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:
// 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// src/main.rs
use marks_report::grade;
fn main() {
for marks in [82, 64, 41] {
println!("{marks} -> {}", grade(marks));
}
}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.rsis ignored until a parent saysmod billing;. - Declaring the same module twice. Write
mod billing;once (inmain.rsorlib.rs), then reach it everywhere withcrate::billingoruse. - Making a struct
puband forgetting its fields. Fields stay private; addpubto fields or provide a constructor and methods. - Making everything
pub. Every public item is a promise to other code. Start private, then open up withpub(crate)orpubwhen needed.
Key points
- A package has a
Cargo.tomland contains crates; a crate is a tree of modules rooted atmain.rsorlib.rs. - Items are private by default;
pub,pub(crate)andpub(super)widen access. Struct fields need their ownpub. - Paths use
crate::(absolute),super::(parent) andself::;usebrings names into scope. mod name;loadsname.rs(orname/mod.rsin older layouts); child modules go in a folder with the parent's name.- Put logic in
lib.rs, keepmain.rsthin, and re-export public items withpub 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
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));
}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?
Items are private unless marked with pub or a restricted form such as pub(crate).
2. Where does mod billing; in src/main.rs look for the module's code?
A mod declaration without a body loads the file with the module's name.
3. You have pub struct Invoice { amount: u64 } in a module. What can code outside the module do?
The struct is public but its field is private, so outsiders need public constructors and methods.
4. What does pub(crate) mean?
pub(crate) shares an item inside your crate without adding it to the public API.
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
