Modules and Crates

Rust's module system controls code organization, visibility, and namespacing. Understanding it is essential for any project beyond a single file.

For Python developers: Think of modules as Python's module system, but with explicit visibility control (public/private). Crates are like Python packages.

Key Terminology

TermMeaningPython Equivalent
CrateA compilation unit - either a binary or a libraryPackage on PyPI
PackageOne or more crates bundled by Cargo (has Cargo.toml)Project with setup.py/pyproject.toml
ModuleA namespace for organizing code within a cratePython module (.py file)
PathHow you name an item (function, struct, etc.)Import path

Packages and Crates

Creating a Package

# Binary crate (has main.rs)
cargo new myapp

# Library crate (has lib.rs)
cargo new --lib mylib

Package Structure

A package can contain multiple crates:

mypackage/
├── Cargo.toml
├── src/
│   ├── main.rs      # Binary crate root (optional)
│   ├── lib.rs        # Library crate root (optional)
│   └── bin/
│       ├── tool1.rs  # Additional binary crate
│       └── tool2.rs  # Another binary crate
# Run the default binary
cargo run

# Run a specific binary
cargo run --bin tool1

Modules

Defining Modules

// src/lib.rs
mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {
            println!("Added to waitlist");
        }

        fn seat_at_table() {
            println!("Seated");
        }
    }

    mod serving {
        fn take_order() {}
        fn serve_order() {}
    }
}

pub fn eat_at_restaurant() {
    // Absolute path
    crate::front_of_house::hosting::add_to_waitlist();

    // Relative path
    front_of_house::hosting::add_to_waitlist();
}

Module Tree

The code above creates this hierarchy:

crate (lib.rs)
└── front_of_house
    ├── hosting
    │   ├── add_to_waitlist (pub)
    │   └── seat_at_table (private)
    └── serving (private)
        ├── take_order
        └── serve_order

Visibility (pub)

Everything in Rust is private by default. Use pub to make items public.

Visibility Rules

mod outer {
    pub mod inner {
        pub fn public_function() {
            println!("public");
            private_function();  // OK: same module
        }

        fn private_function() {
            println!("private");
        }
    }

    fn outer_function() {
        // OK: child module is accessible from parent
        inner::public_function();
        // inner::private_function();  // ERROR: private
    }
}

fn main() {
    outer::inner::public_function();  // OK
    // outer::outer_function();        // ERROR: private
}

Pub for Structs

Fields of a public struct are still private by default:

mod restaurant {
    pub struct Breakfast {
        pub toast: String,      // Public field
        seasonal_fruit: String, // Private field
    }

    impl Breakfast {
        // Need a constructor since seasonal_fruit is private
        pub fn summer(toast: &str) -> Breakfast {
            Breakfast {
                toast: toast.to_string(),
                seasonal_fruit: String::from("peaches"),
            }
        }
    }
}

fn main() {
    let mut meal = restaurant::Breakfast::summer("Rye");
    meal.toast = String::from("Wheat");      // OK: public field
    // meal.seasonal_fruit = "blueberries";  // ERROR: private field
    println!("Toast: {}", meal.toast);
}

Pub for Enums

A public enum has all its variants public:

mod menu {
    pub enum Appetizer {
        Soup,
        Salad,
    }
}

fn main() {
    let order = menu::Appetizer::Soup;  // All variants are public
}

Visibility Modifiers

ModifierVisible To
(none)Current module only
pubEverything
pub(crate)Current crate only
pub(super)Parent module
pub(in path)Specified module
mod outer {
    pub(crate) fn crate_only() {
        println!("visible within crate");
    }

    mod inner {
        pub(super) fn parent_only() {
            println!("visible to outer");
        }
    }

    fn demo() {
        inner::parent_only();  // OK
    }
}

The use Keyword

Bringing Paths into Scope

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

// Bring the module into scope
use front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
}

Idiomatic use Paths

// For functions: bring the parent module (shows where it comes from)
use std::collections::HashMap;
// Then call: HashMap::new()

// For structs/enums: bring the item directly
use std::collections::HashMap;
// Then use: HashMap::new()

// Exception: when two types have the same name
use std::fmt;
use std::io;
// Then: fmt::Result and io::Result

Renaming with as

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

fn function1() -> Result {
    Ok(())
}

fn function2() -> IoResult<()> {
    Ok(())
}

Re-exporting with pub use

mod internal {
    pub fn helper() {
        println!("helping");
    }
}

// Re-export so users of this module can access it directly
pub use internal::helper;

// External code can now call:
// mycrate::helper() instead of mycrate::internal::helper()

Nested use Paths

// Instead of:
use std::collections::HashMap;
use std::collections::HashSet;
use std::collections::BTreeMap;

// Write:
use std::collections::{HashMap, HashSet, BTreeMap};

// Or import everything:
use std::collections::*;

// Nested with self
use std::io::{self, Write};
// Brings in both std::io and std::io::Write

Separating Modules into Files

Single File Per Module

src/
├── main.rs
├── front_of_house.rs        # Module contents
└── front_of_house/
    └── hosting.rs            # Submodule contents
// src/main.rs
mod front_of_house;

fn main() {
    front_of_house::hosting::add_to_waitlist();
}
// src/front_of_house.rs
pub mod hosting;
// src/front_of_house/hosting.rs
pub fn add_to_waitlist() {
    println!("Added to waitlist");
}

Practical Project Layout

myapp/
├── Cargo.toml
├── src/
│   ├── main.rs          # Entry point, minimal code
│   ├── lib.rs           # Library root (optional)
│   ├── config.rs        # Config module
│   ├── database.rs      # Database module
│   ├── models/
│   │   ├── mod.rs       # models module root
│   │   ├── user.rs
│   │   └── product.rs
│   └── handlers/
│       ├── mod.rs       # handlers module root
│       ├── auth.rs
│       └── api.rs
// src/main.rs
mod config;
mod database;
mod models;
mod handlers;

fn main() {
    let cfg = config::load();
    let db = database::connect(&cfg);
    // ...
}
// src/models/mod.rs
pub mod user;
pub mod product;

// Re-export for convenience
pub use user::User;
pub use product::Product;
// src/models/user.rs
#[derive(Debug)]
pub struct User {
    pub name: String,
    pub email: String,
}

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

Now from main.rs you can write:

use crate::models::User;

let user = User::new("Alice", "alice@example.com");

External Crates

Adding Dependencies

# Cargo.toml
[dependencies]
serde = "1.0"
serde_json = "1.0"
rand = "0.8"

Using External Crates

use rand::Rng;
use serde::{Serialize, Deserialize};

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

fn main() {
    let mut rng = rand::thread_rng();
    let n: u32 = rng.gen_range(1..=100);
    println!("Random: {}", n);

    let config = Config {
        name: "myapp".to_string(),
        port: 8080,
    };
    let json = serde_json::to_string(&config).unwrap();
    println!("JSON: {}", json);
}

Version Specification

SpecMeaning
"1.0"Compatible with 1.0 (same as ^1.0)
"^1.0">= 1.0.0, < 2.0.0
"~1.0">= 1.0.0, < 1.1.0
"=1.0.5"Exactly 1.0.5
">=1.0, <2.0"Range

Features

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

# Optional dependencies
reqwest = { version = "0.11", optional = true }

[features]
default = []
networking = ["reqwest"]

Workspaces

For larger projects with multiple related crates:

# Root Cargo.toml
[workspace]
members = [
    "core",
    "cli",
    "server",
]
my-workspace/
├── Cargo.toml        # Workspace root
├── core/
│   ├── Cargo.toml
│   └── src/lib.rs
├── cli/
│   ├── Cargo.toml
│   └── src/main.rs
└── server/
    ├── Cargo.toml
    └── src/main.rs
# cli/Cargo.toml
[dependencies]
core = { path = "../core" }

Practice Exercises

Exercise 1: Library Organization

Create a library with this structure:

  • A math module with add, subtract, multiply, divide
  • A string_utils module with capitalize, reverse, word_count
  • Re-export the most common functions at the crate root

Exercise 2: Multi-File Project

Refactor a single-file program into multiple modules:

  • models module with data structures
  • services module with business logic
  • utils module with helper functions

Exercise 3: Visibility Design

Design a module where:

  • Internal implementation details are private
  • Only a clean public API is exposed
  • A constructor is required because fields are private

Common Mistakes

1. Forgetting pub

mod mymod {
    fn helper() {}  // Private!
}
mymod::helper();  // ERROR

Fix: Add pub to items you need from outside.

2. Wrong File Structure

src/
├── models.rs
└── models/
    └── user.rs    // ERROR: can't have both models.rs and models/

Fix: Use models/mod.rs instead of models.rs, or use only models.rs without the directory.

3. Circular Dependencies

Module A depends on B, and B depends on A. Rust doesn't allow circular crate dependencies.

Fix: Extract shared code into a third module.

Key Takeaways

  • Crates are the unit of compilation; packages are managed by Cargo
  • Modules organize code into namespaces
  • Everything is private by default - use pub to expose
  • use brings paths into scope; pub use re-exports them
  • Split modules into files as projects grow
  • pub(crate) limits visibility to the current crate
  • Workspaces manage multiple related crates

Next Steps

In the next chapter, we'll explore Rust's concurrency features - threads, channels, and shared state.

Quick Reference

// Define a module
mod mymod {
    pub fn public_fn() {}
    fn private_fn() {}
}

// Use a module
use mymod::public_fn;
use std::collections::HashMap;

// Rename
use std::io::Result as IoResult;

// Re-export
pub use internal::helper;

// Nested imports
use std::collections::{HashMap, HashSet};

// File-based modules
// src/mymod.rs or src/mymod/mod.rs
mod mymod;  // in main.rs or lib.rs

// External crate (after adding to Cargo.toml)
use serde::Serialize;

// Visibility
pub fn public() {}
pub(crate) fn crate_only() {}
pub(super) fn parent_only() {}