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 main function) or a library crate (code meant to be used by other crates; no main).

  • Every crate has a crate root, the source file the compiler starts from: by convention src/main.rs for the binary and src/lib.rs for the library. Everything else is reached from the root through mod declarations.

  • A package is one Cargo.toml with one or more crates: at most one library and any number of binaries (src/main.rs, plus src/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.

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:

flowchart TD root["crate (lib.rs)
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) or super:: (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));
}

An unresolvable path is error E0433 ("failed to resolve: use of undeclared crate or module") or E0425/E0412 for values and types; the compiler usually suggests the right use.

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);
}

Glob imports are idiomatic for preludes (use rayon::prelude::*;) and in test modules (use super::*;), and otherwise make it hard to see where names come from. Importing a name that does not exist is E0432; two imports of the same name conflict (E0252) unless one is renamed with as.

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

pub(self)

same as private

pub(super)

the parent module (and its children)

pub(in crate::catalog)

the named ancestor module

pub(crate)

the whole crate, but not outside it

pub

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.

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.rs thin; put logic in lib.rs so 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; pub only for the API you will keep stable.

  • Put unit tests in a #[cfg(test)] mod tests at the bottom of each file, using use super::*;.

  • Split into several crates in a workspace when compile times grow or when parts have different dependencies (the shelf workspace separates shelf-core from the CLI and the web app).

Common errors

Error Cause and fix

E0433 failed to resolve

wrong path or missing use; check crate::/super:: and the module tree

E0432 unresolved import

the item does not exist at that path, or a dependency is missing from Cargo.toml

E0603 …​ is private

make the item (and every module on the path) pub, or re-export it

E0616 field is private

use a getter or constructor, or make the field pub

E0583 file not found for module

create name.rs or name/mod.rs next to the declaring file

"function is never used" (dead_code warning)

a private item nothing calls; in a library, maybe it should be pub

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.