On this page

Modules, crates, and the package system

12 min read TextCh. 5 — Rust in Practice

Rust's module system

Rust organizes code with an explicit, hierarchical module system. Modules control the privacy of items (functions, structs, enums, traits) and organize the namespace.

The hierarchy: packages, crates, and modules

Package
├── Cargo.toml
└── Crates
    ├── Binary crate (src/main.rs) — produces an executable
    └── Library crate (src/lib.rs) — produces a reusable library
        └── Modules
            ├── module::submodule::item
            └── ...

A package can have one or more crates. A crate has a "crate root" (main.rs or lib.rs). Modules organize code within a crate.

Inline modules

// src/main.rs

mod fruits {
    // pub: visible outside the module
    pub struct Fruit {
        pub name: String,
        calories: u32, // private
    }
    
    impl Fruit {
        pub fn new(name: &str, calories: u32) -> Self {
            Fruit {
                name: name.to_string(),
                calories,
            }
        }
        
        pub fn calories(&self) -> u32 {
            self.calories
        }
    }
    
    pub mod tropical {
        use super::Fruit; // super: parent module
        
        pub fn mango() -> Fruit {
            Fruit::new("mango", 60)
        }
        
        pub fn papaya() -> Fruit {
            Fruit::new("papaya", 43)
        }
    }
}

fn main() {
    // Absolute path from the crate root
    let f = fruits::Fruit::new("apple", 52);
    println!("{}: {} cal", f.name, f.calories());
    
    // Access to submodule
    let m = fruits::tropical::mango();
    println!("{}: {} cal", m.name, m.calories());
}

Modules in separate files

For large projects, each module lives in its own file:

src/
├── main.rs
├── math.rs              ← mod math;
└── math/
    ├── mod.rs           (alternative: or use math.rs)
    └── advanced.rs      ← pub mod advanced;
// src/main.rs
mod math; // Looks for src/math.rs or src/math/mod.rs

use math::add;
use math::advanced::factorial;

fn main() {
    println!("3 + 4 = {}", add(3.0, 4.0));
    println!("5! = {}", factorial(5));
}
// src/math.rs
pub mod advanced; // Looks for src/math/advanced.rs

pub fn add(a: f64, b: f64) -> f64 { a + b }
pub fn subtract(a: f64, b: f64) -> f64 { a - b }
pub fn multiply(a: f64, b: f64) -> f64 { a * b }
// src/math/advanced.rs
pub fn factorial(n: u64) -> u64 {
    (1..=n).product()
}

pub fn combinatorial(n: u64, r: u64) -> u64 {
    if r > n { return 0; }
    factorial(n) / (factorial(r) * factorial(n - r))
}

The `use` keyword: importing paths

mod geometry {
    pub mod shapes {
        pub struct Circle(pub f64);
        pub struct Square(pub f64);
    }
}

// Import a specific item
use geometry::shapes::Circle;

// Import multiple items with curly braces
use geometry::shapes::{Circle as C, Square};

// Import everything (glob import) — use carefully
use geometry::shapes::*;

fn main() {
    let c = Circle(5.0);
    let s = Square(4.0);
    println!("Radius: {}, Side: {}", c.0, s.0);
}

Re-exporting with `pub use`

// src/lib.rs

mod implementation {
    pub struct Connection {
        pub url: String,
    }
    
    impl Connection {
        pub fn new(url: &str) -> Self {
            Connection { url: url.to_string() }
        }
    }
}

// Re-export: users can use my_crate::Connection
// instead of my_crate::implementation::Connection
pub use implementation::Connection;

Cargo.toml: dependencies

The Cargo.toml file is the project manifest:

[package]
name = "my-app"
version = "0.1.0"
edition = "2021"
authors = ["David Morales <[email protected]>"]
description = "My Rust application"

[dependencies]
# Semantic versioning
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

# Caret: ^1.0 = >=1.0.0, <2.0.0
tokio = { version = "^1.0", features = ["full"] }

# Tilde: ~1.2 = >=1.2.0, <1.3.0
log = "~0.4"

# Exact version
chrono = "=0.4.38"

# From git
# my-crate = { git = "https://github.com/user/my-crate" }

# From local path (for workspaces)
# my-util = { path = "../my-util" }

[dev-dependencies]
# Only for tests
pretty_assertions = "1"

[build-dependencies]
# For build scripts (build.rs)
cc = "1"

[profile.release]
opt-level = 3
lto = true

Workspaces: multiple crates in one repository

For large projects with multiple related crates:

# Root Cargo.toml (workspace)
[workspace]
members = [
    "core",
    "api-server",
    "cli",
    "utils",
]
resolver = "2"

[workspace.dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
# core/Cargo.toml
[package]
name = "my-core"
version = "0.1.0"
edition = "2021"

[dependencies]
serde.workspace = true  # Uses the workspace version

Essential crates from the ecosystem

Crate Purpose
serde + serde_json Serialization/deserialization
tokio Async runtime
reqwest HTTP client
clap CLI argument parsing
thiserror Error types with derive
anyhow Application error handling
log + env_logger Logging
chrono Dates and times
regex Regular expressions
rand Random numbers

Adding and using a real dependency

cargo add serde --features derive
cargo add serde_json
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
struct Product {
    id: u32,
    name: String,
    price: f64,
    available: bool,
}

fn main() {
    let product = Product {
        id: 1,
        name: String::from("Mechanical Keyboard"),
        price: 89.99,
        available: true,
    };
    
    // Serialize to JSON
    let json = serde_json::to_string_pretty(&product).unwrap();
    println!("{json}");
    
    // Deserialize from JSON
    let json_input = r#"{"id":2,"name":"Mouse","price":29.99,"available":false}"#;
    let product2: Product = serde_json::from_str(json_input).unwrap();
    println!("{:?}", product2);
}

With the module system mastered, we move on to one of Rust's most elegant features: closures and iterators — the functional way to transform data.

pub(crate) vs pub vs pub(super)
Rust has granular visibility: pub makes the item public to everyone, pub(crate) limits it to the current crate, pub(super) limits it to the parent module, and no pub means private to the current module. Prefer pub(crate) over pub when the item is not part of the library's public API.
crates.io and Cargo.lock
Cargo.lock guarantees reproducible builds — it stores the exact versions of all dependencies. For binary projects you should commit Cargo.lock to the repository. For libraries, generally do NOT commit it (consumers of the library will resolve versions themselves).
// src/lib.rs — library crate root

// Inline modules
pub mod math {
    pub fn add(a: f64, b: f64) -> f64 { a + b }
    pub fn subtract(a: f64, b: f64) -> f64 { a - b }

    // Submodule
    pub mod advanced {
        pub fn factorial(n: u64) -> u64 {
            (1..=n).product()
        }

        pub fn fibonacci(n: u32) -> u64 {
            match n {
                0 => 0,
                1 => 1,
                _ => fibonacci(n - 1) + fibonacci(n - 2),
            }
        }
    }
}

// Private module (no pub)
mod internal {
    pub(super) fn helper() -> &'static str { "internal help" }
}

pub fn use_internal() -> &'static str {
    internal::helper()
}

// Re-export with pub use
pub use math::advanced::factorial;