Rust modules and crates
last modified November 13, 2026
In this article we show how to organize Rust code with modules and crates.
A crate is the unit of compilation in Rust. A binary crate
produces an executable and starts at src/main.rs; a library
crate produces code for other crates and starts at src/lib.rs.
A package, created by Cargo, contains one or more crates.
A module is a named container for functions, types, constants,
and other modules. Modules form a tree whose root is the crate. They give
code a structure and control privacy: by default, every item is private to
the module in which it is defined. Items are made visible to the outside with
the pub keyword and are brought into scope with
use.
The examples were tested with Rust 1.97 and the 2024 edition. They are run as
Cargo projects with cargo run -q.
Defining a module
A module is defined with the mod keyword followed by a name and
a block of items.
mod geometry {
pub fn rect_area(width: f64, height: f64) -> f64 {
width * height
}
pub fn square_area(side: f64) -> f64 {
rect_area(side, side)
}
}
fn main() {
println!("{}", geometry::rect_area(3.0, 4.5));
println!("{}", geometry::square_area(5.0));
}
We define a geometry module with two functions.
pub fn rect_area(width: f64, height: f64) -> f64 {
width * height
}
The pub keyword makes the function visible outside the module.
Without it, main could not call the function.
pub fn square_area(side: f64) -> f64 {
rect_area(side, side)
}
Items inside the same module refer to each other by their plain names.
println!("{}", geometry::rect_area(3.0, 4.5));
Outside the module, an item is reached through a path: the module
name, the :: separator, and the item name.
$ cargo run -q 13.5 25
Private items
Items without pub are private. They can be used inside their
module and its child modules, but not from the parent.
mod bank {
pub fn deposit(balance: u64, amount: u64) -> u64 {
audit("deposit", amount);
balance + amount
}
fn audit(operation: &str, amount: u64) {
println!("audit: {operation} {amount}");
}
}
fn main() {
let balance = bank::deposit(100, 50);
println!("balance: {balance}");
bank::audit("manual", 0);
}
The audit function is a private helper of the
bank module. The public deposit function may call
it, because both live in the same module.
bank::audit("manual", 0);
The main function is in the crate root, which is the parent of
bank. The call to the private function is rejected at compile
time with error E0603. Privacy lets a module hide its implementation details
and change them later without breaking the code that uses it.
$ cargo run -q
error[E0603]: function `audit` is private
--> src/main.rs:16:11
|
16 | bank::audit("manual", 0);
| ^^^^^ private function
|
note: the function `audit` is defined here
--> src/main.rs:7:5
|
7 | fn audit(operation: &str, amount: u64) {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
For more information about this error, try `rustc --explain E0603`.
Nested modules and paths
Modules can be nested. A path can start from the crate root with
crate, from the parent module with super, or from
the current module with self.
mod shop {
const TAX_RATE: f64 = 0.2;
pub mod pricing {
pub fn with_tax(price: f64) -> f64 {
self::round(price * (1.0 + super::TAX_RATE))
}
pub fn discounted(price: f64, percent: f64) -> f64 {
round(price * (1.0 - percent / 100.0))
}
fn round(value: f64) -> f64 {
(value * 100.0).round() / 100.0
}
}
pub mod checkout {
pub fn total(prices: &[f64]) -> f64 {
let sum: f64 = prices.iter().map(|&p| super::pricing::with_tax(p)).sum();
crate::shop::pricing::discounted(sum, 10.0)
}
}
}
fn main() {
println!("{}", shop::pricing::with_tax(10.0));
println!("{}", crate::shop::pricing::discounted(19.99, 15.0));
println!("{}", shop::checkout::total(&[4.5, 10.0, 2.25]));
}
The shop module contains two public child modules,
pricing and checkout, and a private constant.
self::round(price * (1.0 + super::TAX_RATE))
The super::TAX_RATE path goes one level up to the
shop module. The constant is private, yet the child module can
read it: privacy hides items from the parents, not from the children. The
self:: prefix is optional here; round alone means
the same.
let sum: f64 = prices.iter().map(|&p| super::pricing::with_tax(p)).sum(); crate::shop::pricing::discounted(sum, 10.0)
The checkout module reaches its sibling through the parent with
a relative path, and through the crate root with an absolute path. Both
paths name the same module. The pricing module itself must be
pub for main to reach its functions; a public
function inside a private module is not visible from outside.
$ cargo run -q 12 16.99 18.09
The use declaration
Long paths are tedious to repeat. The use declaration creates a
shortcut to an item in the current scope.
mod text {
pub mod case {
pub fn upper(s: &str) -> String {
s.to_uppercase()
}
pub fn lower(s: &str) -> String {
s.to_lowercase()
}
}
pub mod stats {
pub fn words(s: &str) -> usize {
s.split_whitespace().count()
}
pub fn chars(s: &str) -> usize {
s.chars().count()
}
}
}
use text::case::{self, upper};
use text::stats::words as word_count;
use text::stats::*;
fn main() {
let s = "An old falcon";
println!("{}", upper(s));
println!("{}", case::lower(s));
println!("{}", word_count(s));
println!("{}", words(s));
println!("{}", chars(s));
}
The example shows the forms of use.
use text::case::{self, upper};
Braces import several names that share a prefix. The self
inside the braces imports the case module itself, so we can
write case::lower as well as the bare upper.
use text::stats::words as word_count;
The as keyword imports an item under a different name. This is
useful when two modules export items with the same name.
use text::stats::*;
The glob import brings in all public items of a module. Globs make it hard to see where a name comes from, so they are used mostly in tests and for prelude modules.
$ cargo run -q AN OLD FALCON an old falcon 3 3 13
Paths to the standard library
Items from the standard library are imported in the same way. The convention is to import types and traits directly, and to import the parent module for functions and for names that would otherwise collide.
use std::collections::HashMap;
use std::fmt::{self, Display};
use std::io::{self, Write};
struct Point {
x: i32,
y: i32,
}
impl Display for Point {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
fn save(out: &mut impl Write, points: &HashMap<&str, Point>) -> io::Result<()> {
let mut names: Vec<_> = points.keys().collect();
names.sort();
for name in names {
writeln!(out, "{name}: {}", points[name])?;
}
Ok(())
}
fn main() -> io::Result<()> {
let mut points = HashMap::new();
points.insert("b", Point { x: -4, y: 0 });
points.insert("a", Point { x: 1, y: 2 });
save(&mut io::stdout(), &points)
}
Some names are available without any import: Vec,
String, Option, Result,
println!, and others come from the prelude, which
the compiler imports into every module. Everything else needs a full path or
a use declaration.
use std::fmt::{self, Display};
use std::io::{self, Write};
Both std::fmt and std::io define a
Result type. We import the modules and write
fmt::Result and io::Result, which keeps the two
apart and leaves the plain Result from the prelude untouched.
The traits Display and Write are imported
directly. A trait must be in scope for its methods to be called; without
Write, the writeln! macro would not compile.
$ cargo run -q a: (1, 2) b: (-4, 0)
Struct fields
A public struct can still have private fields. The module then decides how the struct is created and modified.
mod account {
pub struct Account {
pub owner: String,
balance: u64,
}
impl Account {
pub fn new(owner: &str) -> Self {
Account {
owner: owner.to_string(),
balance: 0,
}
}
pub fn deposit(&mut self, amount: u64) {
self.balance += amount;
}
pub fn balance(&self) -> u64 {
self.balance
}
}
}
use account::Account;
fn main() {
let mut acc = Account::new("Paul");
acc.deposit(120);
acc.deposit(30);
println!("{}: {}", acc.owner, acc.balance());
}
The Account struct is public, the owner field is
public, and the balance field is private.
let mut acc = Account::new("Paul");
acc.deposit(120);
acc.deposit(30);
println!("{}: {}", acc.owner, acc.balance());
Code outside the module can read owner directly, but changes
the balance only through the deposit method and reads it with
the balance getter. The module guarantees that the balance is
never set to an arbitrary value.
$ cargo run -q Paul: 150
Constructing a struct with private fields
A struct with a private field cannot be created with a struct literal outside its module.
mod account {
pub struct Account {
pub owner: String,
balance: u64,
}
impl Account {
pub fn balance(&self) -> u64 {
self.balance
}
}
}
use account::Account;
fn main() {
let acc = Account {
owner: "Lucy".to_string(),
balance: 1_000_000,
};
println!("{}: {}", acc.owner, acc.balance());
}
We try to create an account with a struct expression and a million on the balance.
let acc = Account {
owner: "Lucy".to_string(),
balance: 1_000_000,
};
The compiler rejects the code with error E0451. The only way to create an
Account from outside is through a public associated function
such as new. This is why many library types with private
fields have constructors.
$ cargo run -q
error[E0451]: field `balance` of struct `account::Account` is private
--> src/main.rs:19:9
|
17 | let acc = Account {
| ------- in this type
18 | owner: "Lucy".to_string(),
19 | balance: 1_000_000,
| ^^^^^^^ private field
For more information about this error, try `rustc --explain E0451`.
Enums
Unlike struct fields, enum variants have no visibility of their own. When an enum is public, all its variants are public too.
mod traffic {
#[derive(Debug)]
pub enum Light {
Red,
Yellow,
Green,
}
impl Light {
pub fn next(self) -> Light {
match self {
Light::Red => Light::Green,
Light::Green => Light::Yellow,
Light::Yellow => Light::Red,
}
}
}
}
use traffic::Light;
fn main() {
let mut light = Light::Red;
for _ in 0..4 {
println!("{light:?}");
light = light.next();
}
}
The Light enum is declared pub; the variants need
no extra keyword. A variant that the outside could not see would make
exhaustive matching impossible, so Rust does not allow it.
use traffic::Light;
The enum is imported directly and its variants are written as
Light::Red and so on. The method next is defined
in an impl block and needs its own pub.
$ cargo run -q Red Green Yellow Red
Restricted visibility
Between private and fully public there are restricted forms of
pub. The pub(crate) visibility makes an item public
within the current crate only; pub(super) makes it visible in
the parent module; pub(in path) in a given ancestor module.
mod app {
pub mod config {
pub(crate) fn load() -> Vec<(String, u16)> {
vec![("port".to_string(), port()), ("workers".to_string(), 4)]
}
pub(super) fn port() -> u16 {
8080
}
}
pub fn start() {
println!("starting on port {}", config::port());
}
}
fn main() {
for (key, value) in app::config::load() {
println!("{key} = {value}");
}
app::start();
}
The load function is pub(crate). It can be used
anywhere in this crate, but if the crate were a library, other crates would
not see it. This is the typical visibility of internal helpers in a library.
pub(super) fn port() -> u16 {
8080
}
The port function is visible in config and in its
parent app, where the start function calls it.
$ cargo run -q port = 8080 workers = 4 starting on port 8080
Calling a pub(super) item from outside
A pub(super) item stays hidden from modules above its parent.
mod app {
pub mod config {
pub(super) fn port() -> u16 {
8080
}
}
pub fn start() {
println!("starting on port {}", config::port());
}
}
fn main() {
app::start();
println!("port: {}", app::config::port());
}
The main function is in the crate root, two levels above
config. The port function is visible only up to
app, so the compiler treats it as private here.
$ cargo run -q
error[E0603]: function `port` is private
--> src/main.rs:15:39
|
15 | println!("port: {}", app::config::port());
| ^^^^ private function
|
note: the function `port` is defined here
--> src/main.rs:3:9
|
3 | pub(super) fn port() -> u16 {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
For more information about this error, try `rustc --explain E0603`.
Modules in files
In real projects, modules live in separate files. The declaration
mod name; with a semicolon instead of a block tells the compiler
to load the module body from name.rs next to the current file.
mod inventory;
mod pricing;
use inventory::Item;
fn main() {
let items = [
Item::new("pen", 1.2, 10),
Item::new("notebook", 3.5, 4),
Item::new("eraser", 0.8, 0),
];
for item in &items {
println!(
"{:<10} {:>6.2} in stock: {}",
item.name,
pricing::with_tax(item.price),
item.in_stock()
);
}
println!("stock value: {:.2}", inventory::stock_value(&items));
}
The crate root declares two modules: inventory and
pricing. Each declaration appears once in the whole crate; the
other files refer to the modules with use, never with another
mod.
use crate::pricing;
pub struct Item {
pub name: String,
pub price: f64,
quantity: u32,
}
impl Item {
pub fn new(name: &str, price: f64, quantity: u32) -> Self {
Item {
name: name.to_string(),
price,
quantity,
}
}
pub fn in_stock(&self) -> bool {
self.quantity > 0
}
}
pub fn stock_value(items: &[Item]) -> f64 {
items
.iter()
.map(|item| pricing::with_tax(item.price) * item.quantity as f64)
.sum()
}
The file contains the body of the inventory module, without a
surrounding mod block. The module uses its sibling
pricing through an absolute path starting at
crate. The quantity field is private, so it can
only be set through new.
const TAX_RATE: f64 = 0.2;
pub fn with_tax(price: f64) -> f64 {
price * (1.0 + TAX_RATE)
}
The pricing module has a private constant and one public
function.
$ tree --noreport
.
├── Cargo.toml
└── src
├── inventory.rs
├── main.rs
└── pricing.rs
$ cargo run -q
pen 1.44 in stock: true
notebook 4.20 in stock: true
eraser 0.96 in stock: false
stock value: 31.20
A missing module file
If a mod declaration has no matching file, the build fails. We
add the line mod shipping; to src/main.rs without
creating the file.
mod inventory; mod pricing; mod shipping;
The compiler reports error E0583 and lists the two file names where it
looked for the module: shipping.rs and
shipping/mod.rs.
$ cargo build -q error[E0583]: file not found for module `shipping` --> src/main.rs:3:1 | 3 | mod shipping; | ^^^^^^^^^^^^^ | = help: to create the module `shipping`, create file "src/shipping.rs" or "src/shipping/mod.rs" = note: if there is a `mod shipping` elsewhere in the crate already, import it with `use crate::...` instead For more information about this error, try `rustc --explain E0583`. error: could not compile `shop` (bin "shop") due to 1 previous error
Modules in directories
A module with child modules is split into a file and a directory with the
same name. The store project moves the item type and the report
into their own files inside src/inventory/.
mod inventory;
use inventory::{Item, print_report};
fn main() {
let items = vec![
Item::new("pen", 1.2, 10),
Item::new("notebook", 3.5, 4),
Item::new("eraser", 0.8, 0),
];
print_report(&items);
}
The binary declares the inventory module and imports two items
from it.
mod item; mod report; pub use item::Item; pub use report::print_report;
The inventory.rs file declares the child modules, which the
compiler loads from src/inventory/item.rs and
src/inventory/report.rs. The child modules are private; only the
two re-exported items are visible outside.
pub struct Item {
pub name: String,
pub price: f64,
pub quantity: u32,
}
impl Item {
pub fn new(name: &str, price: f64, quantity: u32) -> Self {
Item {
name: name.to_string(),
price,
quantity,
}
}
}
The item module defines the Item struct.
use super::Item;
pub fn print_report(items: &[Item]) {
for item in items {
println!("{:<10} {:>3} x {:.2}", item.name, item.quantity, item.price);
}
let total: f64 = items.iter().map(|i| i.price * i.quantity as f64).sum();
println!("total: {total:.2}");
}
The report module refers to the struct through its parent
module, where Item was re-exported.
$ tree --noreport src src ├── inventory │ ├── item.rs │ └── report.rs ├── inventory.rs └── main.rs $ cargo run -q pen 10 x 1.20 notebook 4 x 3.50 eraser 0 x 0.80 total: 26.00
Re-exporting with pub use
A plain use creates a private alias that works only inside the
module. With pub use, the alias becomes part of the module's
public interface.
mod item; mod report; pub use item::Item; pub use report::print_report;
Thanks to the re-exports, users of the module write
inventory::Item rather than
inventory::item::Item. The internal file layout is hidden. We
could later move Item to another file, or split the report into
several modules, without changing the code in main.rs. Libraries
use re-exports to offer a flat, stable API on top of a deeper module tree.
The Item struct is the same type under both paths; the
re-export does not create a copy. Since the item module is
private, only the shorter path works outside inventory.
The mod.rs file
Before the 2018 edition, a module with children had to be stored in a
mod.rs file inside its directory. This layout is still
supported. We move src/inventory.rs to
src/inventory/mod.rs; nothing else changes.
Both layouts produce the same module tree. The newer one avoids having many
files named mod.rs open in an editor. Within one project, it is
best to stick with one style.
$ tree --noreport src src ├── inventory │ ├── item.rs │ ├── mod.rs │ └── report.rs └── main.rs $ cargo run -q pen 10 x 1.20 notebook 4 x 3.50 eraser 0 x 0.80 total: 26.00
Both layouts at once
A module must not have both files. We copy mod.rs back to
src/inventory.rs, so the module exists in both forms.
The compiler refuses to choose and reports error E0761.
$ cargo build -q error[E0761]: file for module `inventory` found at both "src/inventory.rs" and "src/inventory/mod.rs" --> src/main.rs:1:1 | 1 | mod inventory; | ^^^^^^^^^^^^^^ | = help: delete or rename one of them to remove the ambiguity For more information about this error, try `rustc --explain E0761`. error: could not compile `store` (bin "store") due to 1 previous error
A library and a binary
A package can contain a library crate and a binary crate at the same time.
The binary then uses the library like any external crate, through the
package name. Logic placed in the library can be tested and reused, while
main.rs stays a thin command-line wrapper.
pub mod case {
pub fn title(s: &str) -> String {
s.split_whitespace()
.map(capitalize)
.collect::<Vec<_>>()
.join(" ")
}
fn capitalize(word: &str) -> String {
let mut chars = word.chars();
match chars.next() {
Some(first) => first.to_uppercase().chain(chars).collect(),
None => String::new(),
}
}
}
pub mod stats {
pub struct Stats {
pub words: usize,
pub chars: usize,
pub vowels: usize,
}
pub fn analyze(s: &str) -> Stats {
Stats {
words: s.split_whitespace().count(),
chars: s.chars().count(),
vowels: s.chars().filter(|c| "aeiouAEIOU".contains(*c)).count(),
}
}
}
pub mod prelude {
pub use crate::case::title;
pub use crate::stats::{Stats, analyze};
}
The src/lib.rs file is the root of the library crate. It
defines the case and stats modules. The
capitalize function is private: it is an implementation detail
of title.
pub mod prelude {
pub use crate::case::title;
pub use crate::stats::{Stats, analyze};
}
A prelude module re-exports the most commonly used items, so that users can import them all with one glob. Many crates follow this pattern.
use std::env;
use textkit::prelude::*;
fn main() {
let text = env::args().skip(1).collect::<Vec<_>>().join(" ");
let Stats { words, chars, vowels } = analyze(&text);
println!("{}", title(&text));
println!("words: {words}, chars: {chars}, vowels: {vowels}");
}
The src/main.rs file is the root of the binary crate. The two
crates are compiled separately: the binary does not declare the library's
modules with mod, it imports them with a path that starts with
the package name, textkit. The binary sees only what the
library made public.
let Stats { words, chars, vowels } = analyze(&text);
The fields of Stats are public, so the binary can destructure
the struct. Command-line arguments come after --.
$ cargo run -q -- an old falcon An Old Falcon words: 3, chars: 13, vowels: 4
Source
The Rust Programming Language: Packages, Crates, and Modules
The Rust Reference: Visibility and privacy
In this article we have organized Rust code with modules and crates. Cargo packages and workspaces are covered in Rust Cargo; struct methods in Rust structs.
Author
List all Rust tutorials.