How to Create an Internal (Private) Module in Rust

Create a private Rust module by defining it without the `pub` keyword to restrict access to its internal functions.

When you need a boundary

You're writing a text parser. Inside your lexer module, you have a function that strips whitespace from a raw buffer. That function is messy. It mutates indices, handles edge cases, and shouldn't be called by the parser module or main. If you leave it exposed, a future you (or a teammate) will call it from the wrong place, and refactoring becomes a nightmare. You need a boundary. You need the function to exist, but only for the code that lives right next to it.

Rust gives you this boundary for free. You don't need special keywords to hide things. You just need to leave them alone.

Privacy is the default

Rust treats privacy as the default. Everything is locked down until you explicitly open the door. A module acts like a container. By default, nothing inside that container is visible to the outside world. You define a module, and its contents stay hidden unless you mark them pub.

This isn't a security feature. It's a design tool. It forces you to think about what your module actually offers versus what it does internally. When you mark something pub, you're making a promise to the rest of the codebase: this interface is stable, and you intend for others to use it. When you leave something private, you're claiming the right to change it later without breaking anyone else.

Think of a restaurant kitchen. The menu is public. Anyone can read it. The recipe cards are private. Customers can't see them, and they shouldn't need to. The chefs use the recipes to make the dishes on the menu. If the kitchen changes a recipe, the menu stays the same. The customers don't care how the soup is made; they just care that the soup exists. In Rust, the module is the kitchen. The pub items are the menu. The private items are the recipes.

Minimal example

Define a module with mod. Items inside are private by default. Mark items pub to expose them.

/// A module demonstrating default privacy rules.
mod internal_utils {
    /// A helper function kept private to this module.
    /// Only code within `internal_utils` can access this.
    fn secret_helper() {
        println!("I am hidden!");
    }

    /// A public interface that exposes functionality safely.
    /// External code calls this, and this delegates to private helpers.
    pub fn public_helper() {
        // Private items are fully accessible to other items
        // inside the same module, regardless of their own visibility.
        secret_helper();
    }
}

fn main() {
    // This compiles. `public_helper` is marked `pub`, so it's visible.
    internal_utils::public_helper();

    // This would cause a compile error.
    // `secret_helper` is private to `internal_utils`.
    // internal_utils::secret_helper();
}

How the compiler checks visibility

The compiler enforces these boundaries at compile time. If you try to access a private item, the build stops. You'll see error[E0603]: function secret_helper is private. The compiler doesn't care about your intentions. It only cares about the pub markers.

There's a crucial distinction between the module itself and the items inside it. Making a module public changes only one thing: other modules can see that the module exists. It does not make the items inside public. You still need pub on the functions or structs inside.

This two-level visibility is a common trap. You can have a public module full of private functions. That's often exactly what you want. It lets you group related public functions under a namespace while hiding the implementation details.

Counter-intuitive but true: pub mod does not make contents public. You need pub on both the module and the item to reach the outside world.

Realistic example

In a real project, you'll use modules to organize code and privacy to control the API. Here's a database module that exposes a connection interface but hides the string formatting logic.

/// Handles database interactions.
/// The module is public, but most items inside are private.
pub mod database {
    /// Constructs the connection URI.
    /// Kept private to prevent external code from depending on the string format.
    fn build_connection_string(host: &str, port: u16) -> String {
        format!("postgres://{}:{}", host, port)
    }

    /// Opens a new database connection.
    /// This is the public API for this module.
    pub fn connect(host: &str, port: u16) {
        // Private helpers are accessible here.
        let uri = build_connection_string(host, port);
        println!("Connecting to {}", uri);
    }
}

fn main() {
    // This works. `connect` is the public interface.
    database::connect("localhost", 5432);

    // This fails. `build_connection_string` is an implementation detail.
    // database::build_connection_string("localhost", 5432);
}

Fine-grained visibility

Sometimes "public" is too broad and "private" is too narrow. You might want to share a helper across multiple modules in your crate, but you don't want library users to access it. Rust provides visibility modifiers for this.

pub(crate) marks an item as public only within the current crate. This is the standard way to share helpers across modules in a library without exposing them to users of the library.

/// A helper visible only within this crate.
pub(crate) fn internal_calculator(x: i32) -> i32 {
    x * 2
}

mod worker {
    /// Uses the crate-private helper.
    pub fn do_work() {
        // This works. `internal_calculator` is visible crate-wide.
        let result = crate::internal_calculator(5);
        println!("Result: {}", result);
    }
}

fn main() {
    // This works. We are inside the same crate.
    let val = internal_calculator(10);
    println!("{}", val);
}

Convention aside: In library crates, prefer pub(crate) over pub for internal helpers. It reduces the API surface and prevents users from relying on implementation details. If you mark something pub, users might depend on it, and you'll be stuck supporting it forever. pub(crate) gives you the flexibility to refactor freely.

There's also pub(super), which makes an item visible only to the immediate parent module. This is useful in deep module hierarchies where you want to expose something to the parent but not to siblings or the crate root.

mod parent {
    mod child {
        /// Visible only to `parent`.
        pub(super) fn secret() {
            println!("Parent knows.");
        }
    }

    /// Accesses the child's super-visible item.
    pub fn reveal() {
        child::secret();
    }
}

fn main() {
    // This fails. `secret` is not visible here.
    // parent::child::secret();
}

Re-exporting to shape your API

You can use pub use to re-export items and control how they appear to users. This lets you hide the module structure while exposing a flat API.

mod internal {
    /// A function hidden inside a private module.
    pub(crate) fn hidden_tool() {
        println!("Tool ready.");
    }
}

/// Re-export the tool at the crate root.
/// This makes `hidden_tool` public, even though the module is private.
pub use internal::hidden_tool;

fn main() {
    // This works. The re-export exposes the function.
    hidden_tool();

    // This fails. The module itself is still private.
    // internal::hidden_tool();
}

pub use can widen visibility. You can re-export a pub(crate) item as pub, making it fully public. You cannot re-export a private item, though. The item must be visible at the re-export site.

Convention aside: Use pub use in lib.rs to flatten the hierarchy for users. If your code is organized as crate::models::user::User, users might prefer crate::User. Re-exporting makes the API ergonomic without changing the internal structure.

Pitfalls

The public module trap is the most common mistake. You write pub mod foo { fn bar() {} } and assume bar is public. It isn't. foo is public. bar is private. You get error[E0603] when you try to call foo::bar() from outside. Fix it by adding pub to bar.

Another pitfall is assuming use bypasses privacy. use is just an alias. It doesn't change visibility. If you write use internal::private_fn;, the compiler rejects it if private_fn is private. You can only use items that are visible to the current scope.

Nested modules add another layer. Visibility is relative to the parent, not the root. If you have mod a { pub mod b { fn c() {} } }, c is public to a, but not to main. You need pub on b and pub on c to reach main. Or use pub(crate) on c to reach the crate root.

Trust the visibility rules. They're strict for a reason. They keep your API clean and your refactoring safe.

Decision matrix

Use a private module (mod name) when the module is a helper for its parent and no other code needs to know it exists.

Use a public module with private items (pub mod name { fn helper() {} }) when you want to group related public functions under a namespace while hiding implementation details.

Use pub(crate) when you need to share an item across multiple modules within the same crate, but you don't want library users to access it.

Use pub on an item when that item is part of the external API and other crates should be able to call it.

Reach for pub(super) when you only need visibility in the immediate parent module, not the whole crate.

Pick pub use when you want to expose an item at a different path or flatten the API for users.

Treat pub as a contract. Once you mark something public, you're responsible for its stability. Keep the public surface small and the private internals free to change.

Where to go next