How to Create Modules in Rust with mod

Create Rust modules using the `mod` keyword and separate files to organize code and control visibility with `pub`.

When one file isn't enough

You start a Rust project. main.rs has twenty lines. It feels clean. You add a database connection, a few helper functions, and a struct for user data. Suddenly main.rs is three hundred lines long. Scrolling takes effort. You can't tell where one concept ends and the next begins. You need a way to group related code and hide the details that don't belong in the main flow.

Rust solves this with modules. A module is a container that groups related items and controls visibility. It lets you organize code by feature, keep helper functions private, and expose only what other parts of the code need. The module system also maps your code to the file system, so your directory structure reflects your code structure.

Modules as containers and namespaces

Think of a module like a folder in a file system. It holds files, but it also defines boundaries. Inside a module, you can define structs, functions, enums, and even other modules. Items inside a module are private by default. Other modules can't see them unless you mark them pub.

This privacy is a feature, not a restriction. It lets you change internal implementation without breaking code that depends on your module. If a function is private, only the module itself uses it. You can rename it, change its arguments, or delete it without touching the rest of the crate. The compiler enforces these rules at compile time. You get an error if you try to access something that isn't exposed.

Visibility is lexical, not based on files. The compiler cares about the module tree, not the file structure. Items in a child module can access pub items in the parent. Items in a sibling module can access pub items in the shared parent. Files are just a convenience for the compiler to find code. The module hierarchy is what matters.

Minimal example: inline and file-based modules

You can declare a module in two ways. Inline modules live in the same file. File-based modules live in separate files. The compiler treats them the same once the code is loaded.

// src/main.rs
// Declare an inline module named `math`.
// The code lives right here in the same file.
mod math {
    /// Add two integers.
    pub fn add(a: i32, b: i32) -> i32 {
        a + b
    }

    // Helper function kept private.
    // Only code inside `math` can call this.
    fn validate_positive(n: i32) -> bool {
        n > 0
    }
}

// Declare a file-based module named `io`.
// Rust looks for `src/io.rs` or `src/io/mod.rs`.
mod io;

fn main() {
    // Call the public function from the inline module.
    let sum = math::add(2, 3);
    println!("Sum: {}", sum);

    // Call the public function from the file-based module.
    io::print_result(sum);
}
// src/io.rs
/// Print a result to stdout.
pub fn print_result(value: i32) {
    println!("Result: {}", value);
}

// Internal helper for formatting.
// Private to the `io` module.
fn format_label(label: &str) -> String {
    format!("[{}] {}", label, value)
}

Inline modules work well for small, tightly coupled code. File-based modules keep large projects navigable. The compiler searches for the file relative to the current file. If you declare mod io; in src/main.rs, it looks for src/io.rs. If you declare mod utils; in src/lib.rs, it looks for src/utils.rs.

Modules are your first line of defense against spaghetti code. Group by purpose, not by file.

How the compiler resolves modules

When the compiler sees mod name;, it performs a file search. It looks for name.rs or name/mod.rs in the same directory as the current file. If it finds the file, it compiles that file as a sibling module in the module tree. The items inside get the name:: prefix.

If the file contains mod child;, the compiler looks for name/child.rs or name/child/mod.rs. This recursive search builds the entire module tree from the file system. The root of the tree is src/lib.rs for libraries or src/main.rs for binaries. Everything else hangs off that root.

The compiler also resolves paths. By default, paths are relative to the current module. super refers to the parent module. crate refers to the root of the crate. self refers to the current module. Absolute paths start with crate:: and work from the root down. Relative paths start with super:: or self:: and navigate the tree.

// src/main.rs
mod outer {
    pub fn hello() {}

    mod inner {
        pub fn call_super() {
            // Access the parent module.
            super::hello();
        }

        pub fn call_crate_root() {
            // Access an item at the crate root.
            // This works regardless of nesting depth.
            crate::outer::hello();
        }
    }
}

Paths are relative by default. Anchor them with crate:: to avoid breakage when you move code around.

Realistic example: library structure with re-exports

Real projects often have a public API that hides internal complexity. You might have a parser module for internal logic and a config module for user-facing types. Users should import my_crate::Config, not my_crate::config::Config. Re-exports let you flatten the API.

// src/lib.rs
// Internal modules. Private by default.
mod parser;
mod config;

// Re-export the public config type.
// Users import `my_crate::Settings`, not `my_crate::config::Settings`.
pub use config::Settings;

/// Parse a configuration string and return settings.
pub fn parse_config(input: &str) -> Result<Settings, parser::Error> {
    // Use the private parser module.
    let raw = parser::parse(input)?;
    // Convert to public config type.
    Ok(Settings::from_raw(raw))
}
// src/config.rs
/// Application settings derived from user input.
pub struct Settings {
    pub verbose: bool,
    pub timeout: u64,
}

impl Settings {
    /// Create settings from raw parsed data.
    pub fn from_raw(raw: parser::RawConfig) -> Self {
        Self {
            verbose: raw.verbose,
            timeout: raw.timeout,
        }
    }
}
// src/parser.rs
/// Internal error type for parsing failures.
// Visible only within the crate.
// Downstream users never see this type.
pub(crate) enum Error {
    MissingField,
    InvalidValue,
}

/// Raw data structure before validation.
pub(crate) struct RawConfig {
    pub(crate) verbose: bool,
    pub(crate) timeout: u64,
}

/// Parse input string into raw config.
pub(crate) fn parse(input: &str) -> Result<RawConfig, Error> {
    // Parsing logic here.
    // Returns an error if input is malformed.
    unimplemented!()
}

Re-exports let you curate the public API. Hide the messy internals behind a clean facade.

Conventions and visibility levels

The Rust community follows a few conventions that pay off in large projects.

Use pub(crate) for items shared within the crate but not exposed to users. This is common for internal helpers, error types, and intermediate structs. It lets you refactor freely without breaking downstream dependencies. If you mark something pub, anyone who depends on your crate can use it. Changing the signature later becomes a breaking change. pub(crate) restricts access to the crate boundary.

Use pub use to flatten the API. Users shouldn't have to type long paths like my_crate::internal::parser::Result. Re-export the important types at the root or in a logical group. This decouples the user experience from your file structure.

Use #[cfg(test)] for test modules. Place tests in the same file as the code they test. The test module can access private items because it lives in the same lexical scope. The cfg attribute ensures the tests only compile during testing, keeping the production binary small.

// src/utils.rs
/// Calculate the factorial of a number.
pub fn factorial(n: u64) -> u64 {
    if n == 0 {
        1
    } else {
        n * factorial(n - 1)
    }
}

// Test module compiled only during `cargo test`.
// Has access to private items in `utils`.
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_factorial_zero() {
        assert_eq!(factorial(0), 1);
    }

    #[test]
    fn test_factorial_five() {
        assert_eq!(factorial(5), 120);
    }
}

Convention aside: Rust 2018 allows src/module.rs for leaf modules. The community prefers this over src/module/mod.rs. It reduces nesting and keeps the directory tree flat. Use mod.rs only when the module contains child modules.

Test modules belong in the same file as the code. They share the private context and catch regressions early.

Pitfalls and compiler errors

Modules introduce a few common traps. The compiler catches most of them, but understanding the errors saves time.

Forgetting pub is the most frequent mistake. You write a function, try to call it from another module, and get E0603 (function is private). The compiler tells you exactly which item is inaccessible. Add pub or pub(crate) to fix it.

Confusing mod and use leads to E0432 (unresolved import). mod declares that a module exists and loads its code. use brings names into the current scope. You need mod before you can use anything from that module. If you skip the mod declaration, the compiler doesn't know the module exists.

Circular dependencies happen when module A imports B and B imports A. The compiler detects the cycle and rejects the code. Fix it by extracting shared types to a third module that both A and B can import. This breaks the cycle and clarifies responsibilities.

Path resolution errors show up as E0433 (failed to resolve). This often happens when you use a relative path that breaks after moving code. Switch to crate:: absolute paths to make imports stable. Absolute paths don't care where the current file lives. They always start from the root.

The compiler enforces privacy. Use it to lock down your internal implementation.

Decision: when to use modules and visibility

Use inline modules when the code is short and tightly coupled to the parent file. Keep it in the same file to avoid context switching.

Use file-based modules when the code grows beyond a few dozen lines. The file system structure makes navigation faster in large projects.

Use pub when you want other modules to access an item. Mark structs, functions, and enums public to expose the API.

Use pub(crate) when an item is shared within the crate but shouldn't leak to downstream users. This is common for internal helpers or error types.

Use pub use when you want to flatten the public API. Re-export types so users don't have to navigate your internal folder structure.

Use mod.rs only when a module has submodules. Prefer name.rs for leaf modules to keep the directory tree flat.

Use #[cfg(test)] when writing unit tests. Place tests in the same file to access private items and keep related code together.

Use crate:: paths when importing across deep nesting. Absolute paths survive refactoring and module moves.

Start simple. Extract to files when the scroll bar appears. Refactor the tree when dependencies tangle.

Where to go next