10. Modules and Crates

📋 Jump to Takeaways

🎁 Imagine a 10,000-line main.rs where every function, struct, and constant lives in one scrollable nightmare. Rust's module system lets you split code into logical pieces — with clear boundaries, explicit visibility, and zero header-file chaos. And when you're ready, you can publish those pieces to crates.io for the world to use.

The mod Keyword — Inline Modules

The simplest module is inline — a named block inside the same file.

mod math {
    pub fn add(a: i32, b: i32) -> i32 {
        a + b
    }

    fn secret() -> i32 {
        42 // private — only accessible inside `math`
    }
}

fn main() {
    println!("{}", math::add(2, 3)); // 5
    // math::secret(); // ERROR: function `secret` is private
}

Everything inside a module is private by default. You opt into visibility with pub.

pub Visibility

pub controls what's accessible from outside the module. You can make functions, structs, fields, and even entire modules public.

mod user {
    pub struct User {
        pub name: String,
        email: String, // private field
    }

    impl User {
        pub fn new(name: &str, email: &str) -> User {
            User {
                name: name.to_string(),
                email: email.to_string(),
            }
        }

        pub fn email(&self) -> &str {
            &self.email
        }
    }
}

fn main() {
    let u = user::User::new("Alice", "[email protected]");
    println!("{}", u.name);      // "Alice" — public field
    // println!("{}", u.email);  // ERROR: field `email` is private
    println!("{}", u.email());   // "[email protected]" — via public method
}

This gives you encapsulation without classes or access modifiers lists — just pub or not.

use Statements

Typing full paths gets tedious. The use keyword brings items into scope.

mod geometry {
    pub mod shapes {
        pub fn circle_area(r: f64) -> f64 {
            f64::consts::PI * r * r
        }
    }
}

use geometry::shapes::circle_area;

fn main() {
    println!("{:.2}", circle_area(3.0)); // 28.27
}

You can also rename imports to avoid conflicts:

use std::fmt::Result as FmtResult;
use std::io::Result as IoResult;

File-Based Modules

In real projects, each module lives in its own file. When you write mod math; the compiler looks for math.rs (or math/mod.rs).

src/
├── main.rs
├── math.rs
└── network/
    ├── mod.rs
    └── tcp.rs
// src/main.rs
mod math;
mod network;

fn main() {
    println!("{}", math::add(1, 2));       // 3
    network::tcp::connect();
}
// src/math.rs
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}
// src/network/mod.rs
pub mod tcp;
// src/network/tcp.rs
pub fn connect() {
    println!("Connected!"); // "Connected!"
}

The directory name matches the module name, and mod.rs declares its sub-modules.

File Structure Conventions

Rust supports two conventions for nested modules:

Old style (mod.rs):

src/network/mod.rs      → mod network
src/network/tcp.rs      → mod tcp (inside network)

New style (Rust 2018+):

src/network.rs          → mod network
src/network/tcp.rs      → mod tcp (inside network)

Both work. The new style avoids dozens of files named mod.rs. Most new projects use the 2018+ convention.

The Crate Root: lib.rs vs main.rs

Every Rust package has a crate root:

  • src/main.rs — binary crate (produces an executable)
  • src/lib.rs — library crate (produces reusable code)

You can have both in one package.

// src/lib.rs — your library's public API
pub mod auth;
pub mod database;

pub fn version() -> &'static str {
    "1.0.0"
}
// src/main.rs — uses the library
use my_app::version;

fn main() {
    println!("App version: {}", version()); // "App version: 1.0.0"
}

The binary crate refers to the library crate by the package name in Cargo.toml.

External Crates and Cargo.toml

To use code from crates.io, add it to Cargo.toml:

[package]
name = "my_app"
version = "0.1.0"
edition = "2021"

[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1", features = ["full"] }
anyhow = "1.0"
clap = { version = "4", features = ["derive"] }

Then use them in your code:

use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug)]
struct Config {
    host: String,
    port: u16,
}

fn main() {
    let config = Config { host: "localhost".into(), port: 8080 };
    let json = serde_json::to_string(&config).unwrap();
    println!("{}", json); // {"host":"localhost","port":8080}
}

Cargo downloads, compiles, and links dependencies automatically.

Common Crates to Know

These are the "standard library extensions" most Rust developers reach for:

Crate Purpose
serde Serialization/deserialization (JSON, TOML, YAML)
tokio Async runtime (networking, timers, I/O)
anyhow Ergonomic error handling for applications
thiserror Derive Error for library error types
clap Command-line argument parsing
reqwest HTTP client
tracing Structured logging and diagnostics
rand Random number generation

You don't need to memorize these — just know they exist so you don't reinvent wheels.

Re-exporting with pub use

Sometimes your internal module structure doesn't match the API you want to expose. pub use re-exports items at a different path.

// src/lib.rs
mod internal {
    pub mod validators {
        pub fn is_valid_email(s: &str) -> bool {
            s.contains('@')
        }
    }
}

// Re-export so users write `my_crate::is_valid_email` instead of
// `my_crate::internal::validators::is_valid_email`
pub use internal::validators::is_valid_email;
// Consumer code
use my_crate::is_valid_email;

fn main() {
    println!("{}", is_valid_email("[email protected]")); // true
}

This keeps your public API clean while your internals stay organized however makes sense.

Workspaces

When your project grows into multiple crates, a workspace ties them together. They share a single target/ directory and Cargo.lock.

# Cargo.toml (workspace root)
[workspace]
members = [
    "core",
    "api",
    "cli",
]
my_project/
├── Cargo.toml          (workspace)
├── core/
│   ├── Cargo.toml
│   └── src/lib.rs
├── api/
│   ├── Cargo.toml
│   └── src/main.rs
└── cli/
    ├── Cargo.toml
    └── src/main.rs

Each member is an independent crate. They can depend on each other:

# api/Cargo.toml
[dependencies]
core = { path = "../core" }

Workspaces keep compile times fast and dependencies consistent across related crates.

Key Takeaways

  • mod declares modules — inline or file-based
  • Everything is private by default; use pub to expose items
  • use brings paths into scope to avoid repetition
  • File-based modules map to filename.rs or filename/mod.rs
  • src/main.rs is the binary root; src/lib.rs is the library root
  • External crates go in Cargo.toml under [dependencies]
  • pub use re-exports items to create a clean public API
  • Workspaces manage multi-crate projects with shared builds

🎁 Your code is organized, your crates are published, but there's more power hiding in plain sight. Next up: Closures — anonymous functions that capture variables from their environment. Rust's ownership rules make them zero-cost abstractions, and they're the foundation of iterators, threads, and async code.

📝 Ready to test your knowledge?

Answer the quiz below to mark this lesson complete.

Spot something off? Report an issue

© 2026 ByteLearn.dev. Free courses for developers. · Privacy