Modules & Crates

Course 2 · Ch 6
Modules & Crates
Organizing a real, growing project — from a single file to a published crate

Course 1 stayed within single small files. This chapter covers organizing a real Rust project as it grows: modules within a crate, Cargo.toml in depth, workspaces for multi-crate projects, and finally publishing a crate to crates.io.

The Module System

mod front_of_house { pub mod hosting { pub fn add_to_waitlist() {} } } front_of_house::hosting::add_to_waitlist();

mod organizes code into a tree of namespaces within a single crate. Everything is private by default — a genuinely different default from many languages. pub makes an item visible to its parent module — not globally, a common early misconception. pub(crate) exposes an item crate-wide, but not to external code.

Splitting Modules Across Files

A module can live in its own file: mod front_of_house; (no body) tells Rust to look for the module's contents elsewhere. The modern convention (2018 edition onward) needs no mod.rs file:

src/ lib.rs // mod front_of_house; front_of_house.rs // pub mod hosting; front_of_house/ hosting.rs // pub fn add_to_waitlist() {}

use and Bringing Paths Into Scope

use crate::front_of_house::hosting; use std::collections::HashMap as Map; // renaming to avoid a clash pub use crate::front_of_house::hosting::add_to_waitlist; // re-export at the crate root

use shortens repeated long paths — already in constant use since Course 1's use std::collections::HashMap (Chapter 8). pub use re-exports an item, making something defined deep in the module tree part of the crate's own public API surface at a shallower, more convenient path.

Cargo.toml in Depth

[package] name = "my_crate" version = "0.1.0" edition = "2021" [dependencies] serde = "1.0" # caret by default: allows 1.x, never 2.0

Cargo.lock pins the exact, fully-resolved dependency versions actually used — directly analogous to npm's package-lock.json. Committing it for a binary/application ensures reproducible builds across machines; libraries conventionally don't commit it, since consumers resolve their own compatible versions.

Workspaces: Multiple Crates, One Project

# root Cargo.toml [workspace] members = ["app", "core-lib"]

A workspace shares one Cargo.lock and one target/ build directory across every member crate — the standard pattern once a project grows past a single crate, e.g. splitting a binary crate from the library crate(s) it depends on.

Publishing to crates.io

cargo publish requires a globally unique crate name (crates.io names can't be reused once claimed), required metadata (license, description), and an API token. Publishing is permanent for a given version number — a published version can be yanked (hidden from new dependents) but never overwritten or deleted outright.

npm (JavaScript)Cargo (Rust)
Lock filepackage-lock.jsonCargo.lock
PurposePin exact resolved versions for reproducibilityIdentical purpose
Committed for libraries?Typically notTypically not (unlike applications, which do)

mod / pub

Organizes code into namespaces; private by default, pub exposes to the parent module.

File-Based Modules

mod name; points at name.rs, with name/submodule.rs for its own children.

Cargo.toml / Cargo.lock

Package metadata and dependencies; the lock file pins exact resolved versions.

Workspaces

Multiple crates sharing one lock file and build directory under one root Cargo.toml.

Private-by-Default Is the Same Philosophy Again
Rust's modules being private unless explicitly marked pub is the same "safety by default" instinct behind Course 1's immutable-by-default variables — an accidentally-exposed item requires deliberately opting in with pub, rather than requiring someone to remember to lock it down after the fact.
pub Alone Doesn't Guarantee External Visibility
Marking a deeply-nested item pub only exposes it to its immediate parent module — it does not automatically make it reachable from outside the crate. Every module along the path to that item must also be pub (or the item must be re-exported via pub use at a more accessible path). Visibility has to be "pub all the way up," not just at the final leaf item — a genuinely common early confusion.

Coding Challenges

Challenge 1

Write a module tree: a top-level module shop containing a submodule inventory with a public function check_stock(). Show the full path needed to call check_stock() from outside the shop module, and explain what pub keywords are required at each level.

📄 View solution
Challenge 2

Write a Cargo.toml for a binary crate named "task_tracker" at version 0.1.0, using the 2021 edition, depending on serde version "1.0" and clap version "4.0". Explain what Cargo.lock would add on top of this file once the project is built.

📄 View solution
Challenge 3

Explain the specific bug in this module tree: mod a { pub mod b { pub fn f() {} } } where a itself is NOT marked pub — why can code outside the crate still not call a::b::f() from outside, even though both b and f are marked pub?

📄 View solution

Chapter 6 Quick Reference

  • mod name { ... } — declares a module; items are private by default
  • pub — exposes an item to its parent module; pub(crate) — exposes crate-wide only
  • mod name; (no body) — loads the module from name.rs, with name/ for its own submodules
  • use path::to::item; — shortens repeated paths; pub use — re-exports at a shallower path
  • Cargo.toml — package metadata + dependencies; Cargo.lock — exact resolved versions, like npm's package-lock.json
  • [workspace] members = [...] — multiple crates sharing one lock file and build directory
  • cargo publish — permanent per version; a bad publish can only be yanked, never overwritten
  • pub must apply all the way up the module path, not just at the final item, for external code to actually reach it
  • Next chapter: Testing in Rust — #[test], cargo test, unit vs. integration tests, contrasted with Go's built-in testing package