ZetCode

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.

main.rs
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.

main.rs
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.

main.rs
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.

main.rs
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.

main.rs
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.

main.rs
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.

main.rs
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.

main.rs
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.

main.rs
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.

main.rs
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.

src/main.rs
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.

src/inventory.rs
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.

src/pricing.rs
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/.

src/main.rs
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.

src/inventory.rs
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.

src/inventory/item.rs
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.

src/inventory/report.rs
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.

src/lib.rs
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.

src/main.rs
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

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Rust tutorials.