Rust Module Visibility
debt(d7/e5/b5/t7)
Closest to 'only careful code review or runtime testing' (d7). Over-exposing items with bare `pub` compiles fine — the compiler never errors on something being too public. Clippy has some lints (e.g. redundant_pub_crate) but the core risk of committing internal helpers to a stable public API is silent unless caught in code review or API-diff tooling. The code_pattern regex flags pub struct with pub fields but most accidental over-exposure passes unnoticed.
Closest to 'touches multiple files / significant refactor in one component' (e5). The quick_fix (default to private, use pub(crate), keep fields private behind accessors) is simple in isolation, but once a `pub` item has leaked into a published API, tightening visibility is a breaking change that ripples through all call sites referencing it and may require adding constructors/accessors across the module.
Closest to 'persistent productivity tax' (b5). applies_to spans library/web/cli/queue-worker contexts, and visibility choices form the public API contract; over-exposed items create a maintenance obligation across versions that shapes future refactors. It slows many work streams without fully defining system shape.
Closest to 'serious trap' (t7). The misconception is explicit: developers assume marking a struct `pub` makes all fields public, when each field needs its own `pub`. This contradicts the mental model from many other languages where public class implies accessible members, and the path-visibility rule (every module in the path must be pub) is a further documented gotcha.
Also Known As
TL;DR
Explanation
Rust organizes code into a tree of modules, and every item - functions, structs, enums, fields, constants, modules themselves - has a visibility that decides who may name it. The default is private, meaning an item is visible only within the module that defines it and that module's descendants. This is the opposite of many languages where everything is public unless hidden, and it is the foundation of Rust's encapsulation: nothing leaks out of a module until you deliberately export it.
The `pub` keyword makes an item visible to outside code, but visibility is also subject to the path of re-exports along the way. An item must be `pub` and every module in its path must also be `pub` for an external crate to reach it. For finer control Rust offers restricted forms. `pub(crate)` exposes an item to the whole current crate but never beyond it, which is the right choice for internal helpers shared across modules that should not become part of your public API. `pub(super)` limits visibility to the immediate parent module. `pub(in path)` restricts visibility to a specific ancestor module by path. A bare `pub(self)` is equivalent to private.
Struct fields deserve special attention. Making a struct `pub` does not make its fields public; each field has its own visibility. A struct with private fields can only be constructed inside its module, which lets you enforce invariants through constructor functions and prevents callers from depending on internal layout. This is how Rust types guarantee they are always in a valid state.
A common discipline is to keep the public surface minimal. Mark internal-only items `pub(crate)` rather than `pub` so they cannot become accidental API commitments that you must support across versions. Use `pub use` re-exports to present a clean, flat public path while keeping your internal module structure private. Getting visibility right means your crate's API is exactly what you intend, the compiler enforces encapsulation, and refactoring internal modules never breaks downstream users.
Common Misconception
Why It Matters
Common Mistakes
- Marking helpers `pub` when `pub(crate)` is enough, accidentally committing internal functions to your stable public API.
- Assuming a `pub struct` exposes its fields, when each field needs its own `pub` to be accessible.
- Forgetting that an item only reaches an external crate if every module in its path is also `pub`.
- Making struct fields public and losing the ability to enforce invariants through controlled constructors.
- Using `pub(super)` or `pub(in path)` without understanding the module tree, exposing items to the wrong scope.
Avoid When
- Building a binary crate with no external consumers, where the public/private distinction has little practical impact.
- Rapid prototyping where locking down visibility slows iteration and there is no API to protect yet.
- A field genuinely is plain data with no invariant, where a public field is simpler than boilerplate accessors.
When To Use
- Designing a library crate where the public API must stay minimal and stable across versions.
- Enforcing struct invariants by keeping fields private and exposing controlled constructors and methods.
- Sharing helpers across modules of one crate without exposing them externally, using `pub(crate)`.
- Re-exporting a clean public path with `pub use` while keeping the internal module layout private.
Code Examples
// lib.rs
pub mod store {
// Everything is pub, so callers depend on internals.
pub struct Account {
pub balance: i64, // public field: no invariant enforcement
}
// Internal helper leaked into the public API.
pub fn audit_log(msg: &str) {
println!("audit: {}", msg);
}
}
fn main() {
// Callers can mutate balance directly into an invalid state.
let mut a = store::Account { balance: 100 };
a.balance = -500; // overdraft, no check possible
store::audit_log("tampered");
println!("{}", a.balance);
}
// lib.rs
pub mod api {
// Private helper - only this module
fn validate_input(x: i32) -> bool {
x > 0
}
// Public struct with public field
pub struct Request {
pub id: u64,
count: i32, // Private field - use getter
}
impl Request {
pub fn new(id: u64, count: i32) -> Self {
Request { id, count }
}
// Public getter for private field
pub fn count(&self) -> i32 {
self.count
}
}
// Shared across crate, not part of public API
pub(crate) fn internal_helper() {}
}
pub mod utils {
// Only visible in parent module
pub(super) fn sibling_only() {}
}
// Private module - not exposed outside crate
mod private {
pub fn hidden() {} // Still private to external crates
}