Modules & Project Layout
Everything so far lived in one main.rs. Real projects don't - they grow into many files, pull in code
other people wrote, and split logic into tidy named groups. This phase is the map: what each file in a
Cargo project is for, how to carve code into modules, what pub and use actually do, and what
people mean by a crate. None of this is hard - it's mostly naming things you've half-noticed already.
This is also the last phase before the big one: Phase 6: Ownership, the phase the whole guide has been walking toward.
Anatomy of a Cargo project
Remember what cargo new hello made back in Phase 1? Let's read it now
that the pieces will mean something:
$ ls hello
Cargo.toml src
$ ls hello/src
main.rs
Two things matter:
Cargo.toml - the manifest. A small config file describing your project: name, version, and the
libraries it depends on. You'll edit this whenever you add a dependency.
[]
= "hello"
= "0.1.0"
= "2024"
[]
What just happened: [package] names your project and pins the edition (a several-year batch of
language conventions - 2021 and 2024 are both fine; cargo new picks a recent one for you).
[dependencies] is empty for now - where added libraries get listed.
📝 Terminology. Cargo.toml is the file you edit. You'll also see a Cargo.lock appear next to it -
written by cargo to record the exact versions it resolved, so builds are reproducible. Leave
Cargo.lock to cargo; don't hand-edit it.
src/main.rs vs src/lib.rs - the two kinds of crate root. Worth memorizing:
src/main.rsis the entry point of a binary - a program you run. It has afn main(). This is whatcargo newgives you by default.src/lib.rsis the entry point of a library - code meant to be used by other code, not run on its own. It has nomain. You get one withcargo new --lib mylib.
💡 Key point. A binary is a thing you run; a library is a thing you reuse. Many real projects have
both: a lib.rs holding the actual logic (easy to test and share) and a thin main.rs that calls into it.
For now, main.rs is all you need.
mod - group code into modules
What it actually is. A module is a named container for related code - functions, types, other
modules. It's how you keep a growing file from becoming a wall of unrelated functions. Declare one with
mod.
$ cargo run
Hello, Ada!
What just happened: mod greetings { ... } created a module holding one function. From outside, reach
into it with greetings::hello(...) - :: is the path separator, "the hello inside greetings."
(format! is like println! but builds a String instead of printing it.)
📝 Terminology. A path like greetings::hello names where something lives - module by module,
separated by ::. You've already used paths: std::collections::HashMap in Phase 3
is "HashMap, inside collections, inside the standard library std."
pub - make things public
Did you notice the pub on hello? That's not decoration. By default, everything in a module is
private - usable only inside that module. pub ("public") opens it up to the outside.
$ cargo run
error[E0603]: function `secret` is private
--> src/main.rs:13:31
|
13 | println!("{}", greetings::secret());
| ^^^^^^ private function
|
note: the function `secret` is defined here
--> src/main.rs:6:5
|
6 | fn secret() -> &'static str {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
What just happened: hello is pub, so calling it works. secret has no pub, so it's private to the
greetings module, and the compiler refuses to let main reach in and call it. Code inside
greetings could still call secret freely; only the outside is blocked.
💡 Key point. Private-by-default is the same philosophy as immutable-by-default from Phase 2: Rust makes the safe, restrictive choice the default and asks you to opt out. The benefit is a clear contract - anyone reading your module sees exactly which functions are meant to be used and which are internal plumbing they shouldn't depend on.
use - bring a name into scope
Typing greetings::hello every time gets old, and long paths like std::collections::HashMap get very
old. use pulls a name into the current scope so you can refer to it by its short name:
use hello; // bring `hello` into scope
$ cargo run
Hello, Ada!
What just happened: use greetings::hello; made hello available by its bare name in main. Same
reason you wrote use std::collections::HashMap; before using HashMap in Phase 3 -
use shortens the path; it doesn't change what the thing is.
Modules can nest, giving your project a tree you can picture:
Reading the tree: the crate root (main.rs) holds two modules; greetings exposes hello but keeps
secret private; math exposes add and has its own nested module. A path like math::add walks this
tree from the root down.
Crates and dependencies
📝 Terminology. A crate is the unit Rust compiles: one binary or one library. Your project is a crate, and the libraries you pull in are crates too - published on crates.io, Rust's package registry (like npm for JavaScript or PyPI for Python).
To use someone else's crate, add it to Cargo.toml - easiest via cargo add, which edits the file for
you:
$ cargo add rand
Updating crates.io index
Adding rand v0.9.2 to dependencies
What just happened: cargo add rand looked up the rand crate (random numbers), wrote it into
[dependencies], and noted the version. The next cargo build or cargo run downloads and compiles it
automatically - that's the build whose first run is a bit slow, as flagged in
Phase 1. After that, bring its items in with use (e.g.
use rand::Rng;) just like your own modules. That's the whole point of cargo: this short.
⚠️ Gotcha - mod vs use confusion. These do different jobs. mod foo; declares that a module
exists (and, in a multi-file project, tells Rust to load foo.rs). use foo::bar; brings an existing
name into scope for convenience. Declare a module once with mod; use names from it as often as
you like. "Unresolved import" usually means a use for something never mod-declared (or never pub).
You're ready for the hard part
You can now write Rust that holds data, makes decisions, lives in functions, and is organized into modules across a real project - a genuine working foundation, most of a programming language.
But there's one idea we've been circling: every time you saw a & (borrowing a Vec in a for loop,
passing &owned where a &str was wanted, &str itself), a deeper system was at work. That system is
ownership, the reason Rust can promise memory safety without a garbage collector.
🪖 A word before you turn the page. Phase 6 is the phase. It's where Rust stops resembling languages you know and the borrow checker - the part of the compiler that enforces ownership - starts rejecting code that looks fine to you. Everyone struggles here; it's the normal shape of learning Rust, not a sign you're doing badly. Take it slowly, type the examples, and let the mental model build - once ownership clicks, the rest of Rust falls into place around it. For the "why does any language need this?" background first, see Memory & Garbage Collection.
Recap
Cargo.tomlis the manifest you edit (name, edition, dependencies);Cargo.lockis managed bycargo.src/main.rsis a binary you run (fn main);src/lib.rsis a library other code reuses.modgroups code into modules; reach into them with::paths.- Everything is private by default;
pubexposes it. Privacy is a deliberate, opt-out contract. useshortens a path by bringing a name into scope - it doesn't change what the thing is.- A crate is a compiled unit (your project, or a library);
cargo add <name>pulls one from crates.io.
Next: the heart of Rust - ownership and borrowing. Who owns a value, what "moving" and "borrowing" really mean, and why the borrow checker is on your side even when it's saying no.
← Phase 4: Control Flow & Functions · Guide overview · Phase 6: Ownership & Borrowing →
Before the quiz: without looking back, say (or jot down) the core idea of this phase in your own words.
Check your understanding 3 questions
1. What is the difference between src/main.rs and src/lib.rs?
2. By default, what is the visibility of items in a Rust module?
3. What is the difference between `mod` and `use`?