Modules, Crates & Visibility
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Rust 1.99.0 and the 2024 edition, checked on 2026-10-06. Rust organises code at two levels:
crates are the unit of compilation and distribution, and modules organise code inside a crate and control what is
visible where. Cargo adds a third level, packages and workspaces, covered on
Cargo & Workspaces. The examples use the shelf workspace’s
library crate, shelf-core.
Packages, crates and crate roots
-
A crate is the smallest unit the compiler processes. It is either a binary crate (an executable with a
mainfunction) or a library crate (code meant to be used by other crates; nomain). -
Every crate has a crate root, the source file the compiler starts from: by convention
src/main.rsfor the binary andsrc/lib.rsfor the library. Everything else is reached from the root throughmoddeclarations. -
A package is one
Cargo.tomlwith one or more crates: at most one library and any number of binaries (src/main.rs, plussrc/bin/*.rs), examples, tests and benchmarks.
When a package has both lib.rs and main.rs, the binary uses the library like any external crate, through the
package name with - replaced by _ (shelf_core::Catalog). That split — logic in the library, a thin main.rs — makes the logic testable and reusable, and the shelf CLI and the Bookshelf web app both depend on the same library.
Source: The Book: Packages and Crates
The module tree
A crate is a tree of modules rooted at the crate root, whose implicit name is crate. A module is declared with mod
name and contains items: functions, types, traits, constants, other modules. Its body can be written inline:
mod catalog {
pub mod search {
pub fn by_title(titles: &[&str], query: &str) -> Vec<String> {
titles.iter().filter(|t| t.to_lowercase().contains(query)).map(|t| t.to_string()).collect()
}
}
pub fn summary(titles: &[&str]) -> String {
format!("{} titles, {} match 'dune'", titles.len(), search::by_title(titles, "dune").len())
}
}
fn main() {
let titles = ["Dune", "Dune Messiah", "Emma"];
println!("{}", catalog::summary(&titles));
println!("{:?}", catalog::search::by_title(&titles, "emma"));
}
Modules in files
mod catalog; with a semicolon tells the compiler to load the module’s body from a file. For a module declared in
src/lib.rs (or main.rs), it looks in:
-
src/catalog.rs(the modern style, since the 2018 edition), or -
src/catalog/mod.rs(the older style, still supported).
A submodule mod search; declared inside src/catalog.rs lives in src/catalog/search.rs. Having both catalog.rs and
catalog/mod.rs is an error, and a missing file is error E0583. Note
that files are not modules by themselves: a .rs file is compiled only if some mod declaration reaches it.
shelf-core/
├── Cargo.toml
└── src/
├── lib.rs # crate root: mod catalog; mod isbn; mod model; pub use ...
├── catalog.rs # mod catalog (contains: mod memory;)
├── catalog/
│ └── memory.rs # mod catalog::memory
├── isbn.rs # mod isbn
└── model.rs # mod model
The shelf-core crate is organised like this; the public API is re-exported from the root so users do not need to know the internal layout:
pub use catalog::{Catalog, CatalogError, InMemoryCatalog}
pub use isbn::{Isbn, IsbnError}
pub use model::{Author, Book, Loan, LoanState}"] root --> catalog["mod catalog (private)
pub trait Catalog
pub enum CatalogError"] catalog --> memory["mod memory (private)
pub struct InMemoryCatalog"] root --> isbn["mod isbn (private)
pub struct Isbn
pub enum IsbnError"] root --> model["mod model (private)
pub struct Book, Author, Loan
pub enum LoanState"] root --> tests["#[cfg(test)] mod tests"]
Paths
Items are named by paths separated by :::
-
absolute paths start with
crate::(this crate) or an external crate’s name (std::,serde::); -
relative paths start from the current module, with
self::(this module) orsuper::(the parent module) when needed.
mod model {
#[derive(Debug)]
pub struct Book {
pub title: String,
}
pub mod loans {
pub fn describe(book: &super::Book) -> String { // super:: = the parent module
format!("loan of {}", book.title)
}
pub fn shelf_label(book: &crate::model::Book) -> String { // absolute path
self::prefix() + &book.title // self:: = this module
}
fn prefix() -> String {
"SHELF/".into()
}
}
}
fn main() {
let b = model::Book { title: "Dune".into() };
println!("{} | {}", model::loans::describe(&b), crate::model::loans::shelf_label(&b));
}
use declarations
use brings a path into scope under a shorter name. Conventions from The Book: for functions, import the parent
module and call module::function() (it shows where the function comes from); for types, traits and enums, import the
item itself.
use std::collections::{BTreeMap, HashMap}; // nested import
use std::fmt::{self, Display}; // `self` imports the module too
use std::io::Result as IoResult; // rename with `as`
use std::sync::atomic::*; // glob import (use sparingly)
struct Count(usize);
impl Display for Count {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} books", self.0)
}
}
fn load() -> IoResult<usize> {
Ok(3)
}
fn main() {
let mut by_genre: HashMap<&str, usize> = HashMap::new();
by_genre.insert("sci-fi", 2);
let sorted: BTreeMap<_, _> = by_genre.into_iter().collect();
let counter = AtomicUsize::new(0);
counter.fetch_add(1, Ordering::Relaxed);
println!("{sorted:?} {} {:?}", Count(load().unwrap_or(0)), counter);
}
Re-exports with pub use
pub use imports a name and makes it part of this module’s public API. Libraries use it to present a flat, stable API
regardless of internal structure — shelf_core::Isbn instead of shelf_core::isbn::Isbn — and to re-export
dependencies' types that appear in their own signatures:
mod shelf_core {
mod isbn {
#[derive(Debug)]
pub struct Isbn(pub String);
}
mod catalog {
pub trait Catalog {
fn len(&self) -> usize;
}
}
pub use catalog::Catalog; // public at shelf_core::Catalog
pub use isbn::Isbn; // public at shelf_core::Isbn, though `isbn` itself is private
}
use shelf_core::{Catalog, Isbn};
struct Empty;
impl Catalog for Empty {
fn len(&self) -> usize {
0
}
}
fn main() {
println!("{:?} {}", Isbn("9780441013593".into()), Empty.len());
// shelf_core::isbn::Isbn(...) would not compile: module `isbn` is private (E0603)
}
rustdoc shows re-exported items where they are re-exported; see Documentation with rustdoc.
Visibility
Everything is private by default: visible in the module where it is defined and in that module’s descendants. pub
and its restricted forms widen that:
| Visibility | Visible in |
|---|---|
(none) |
the current module and its children |
|
same as private |
|
the parent module (and its children) |
|
the named ancestor module |
|
the whole crate, but not outside it |
|
everywhere the parent module is visible — including other crates, if the path to it is public |
mod catalog {
pub struct Catalog {
pub name: String, // public field
pub(crate) books: Vec<String>, // visible anywhere in this crate
loans: u32, // private: only inside `catalog`
}
impl Catalog {
pub fn new(name: &str) -> Self {
Catalog { name: name.into(), books: Vec::new(), loans: 0 }
}
pub fn lend(&mut self) {
self.loans += 1;
audit::record(self.loans);
}
}
mod audit {
pub(super) fn record(n: u32) { // only `catalog` may call this
println!("loan #{n} recorded");
}
}
}
fn main() {
let mut c = catalog::Catalog::new("main");
c.books.push("Dune".into()); // ok: pub(crate)
c.lend();
println!("{} {}", c.name, c.books.len());
// c.loans; // would not compile: field `loans` is private (E0616)
// catalog::audit::record(1); // would not compile: module `audit` is private (E0603)
}
Two details catch people out. A pub item inside a private module is still unreachable from outside (the path must be
public), unless it is re-exported. And for an enum, making the enum pub makes all its variants and their fields public,
while a struct’s fields stay private unless each one is marked pub — so a struct with a private field can only be
constructed through its constructor, which is how types such as Isbn enforce invariants. Mara & Holmes (Refactoring to
Rust, chapter 5) recommend pub(crate) for internal helpers shared across modules; the unreachable_pub lint flags
pub items that are not actually reachable from outside.
Preludes
Every module implicitly imports the standard prelude — Option, Result, Vec, String, Box, Clone, Iterator,
ToString, drop and so on. The prelude depends on the edition: 2021 added TryFrom, TryInto and FromIterator, and
2024 added Future and IntoFuture. There are also the extern prelude (all crates passed with --extern, i.e. your
dependencies), the language prelude (primitive types) and tool preludes. Libraries often provide their own prelude
module for users to glob-import.
Source: The Rust Reference: Preludes
extern crate (legacy)
Before the 2018 edition, every dependency had to be declared with extern crate serde; in the crate root. Today
dependencies listed in Cargo.toml are in scope automatically. extern crate is still needed only in special cases:
extern crate alloc; in a no_std crate, or extern crate self as my_crate; for macro hygiene.
Structuring a library
Guidelines distilled from The Book, the API Guidelines and Refactoring to Rust (chapter 5):
-
Keep
main.rsthin; put logic inlib.rsso it can be tested and reused. -
Organise modules by domain concept (
catalog,isbn,loans), not by kind of item (structs,traits). -
Keep modules private and re-export a deliberate public API from the crate root (or a few public modules).
-
Use
pub(crate)for helpers shared across modules;pubonly for the API you will keep stable. -
Put unit tests in a
#[cfg(test)] mod testsat the bottom of each file, usinguse super::*;. -
Split into several crates in a workspace when compile times grow or when parts have different dependencies (the shelf workspace separates
shelf-corefrom the CLI and the web app).
Common errors
| Error | Cause and fix |
|---|---|
E0433 failed to resolve |
wrong path or missing |
E0432 unresolved import |
the item does not exist at that path, or a
dependency is missing from |
E0603 … is private |
make the item (and every module on the path)
|
E0616 field is private |
use a getter or constructor, or make the field
|
E0583 file not found for module |
create |
"function is never used" ( |
a private item nothing calls; in a library, maybe it should be |
Where the books are outdated
Mara & Holmes (Refactoring to Rust, chapters 1-2 and 5) describe modules, files, paths, use aliases and
pub(crate)/pub(super) for the 2018 edition; the module system has not changed since, but the 2021 and 2024 preludes
add TryFrom/TryInto/FromIterator and Future/IntoFuture, which older code imports explicitly.
References
-
The Rust Programming Language: Packages, Crates, and Modules
-
Rust by Example: Modules (visibility, struct visibility, use, super and self, file hierarchy)
-
Lily Mara and Joel Holmes, Refactoring to Rust (Manning, 2025)