Crates and Packages
A crate is the smallest amount of code the Rust compiler treats as a single compilation unit — either a binary (something that produces a runnable program) or a library (something other code links against). A package is the unit that Cargo, Rust’s build tool, understands: a bundle of one or more crates described by a single Cargo.toml manifest. Understanding this distinction is the key to understanding how any real Rust project is put together, from a one-file CLI tool to a codebase made of dozens of internal libraries.
Overview: How Crates and Packages Fit Together
Every Rust program you have written so far in this course, even a single file with a fn main, is already a crate — specifically a binary crate, because it produces an executable. Rust also has library crates, which have no main function at all; instead they expose a set of public items (functions, types, traits) for other crates to use. When you write use std::collections::HashMap;, you are reaching into the standard library, which is itself a crate — just one that ships with the compiler and is linked into every project automatically, so you never declare it as a dependency.
The compiler’s job starts at a single file called the crate root. For a binary crate this is conventionally src/main.rs; for a library crate it’s src/lib.rs. Rust builds up the rest of the crate from that root using mod declarations, which tell the compiler "this crate also contains a module defined elsewhere" (the next lesson covers the mechanics of mod, pub, and multi-file layout in depth). The crate root plus everything reachable from it via mod forms one compilation unit, with one namespace, one set of privacy rules, and one final compiled artifact.
A package sits one level above a crate. It’s what cargo new my_project creates for you: a directory containing a Cargo.toml manifest (metadata: name, version, edition, dependencies) plus a src/ directory with the actual crate source. A package can contain at most one library crate, but as many binary crates as you like — each additional file placed in src/bin/ becomes its own binary crate compiled from the same package. A common pattern is a package that publishes one library crate with the real logic, plus one or more thin binary crates in src/bin/ that just call into that library.
A useful mental model: think of a crate as a shipping container — one sealed unit the compiler builds all at once — and a package as the paperwork and depot around it: the manifest that names it, versions it, and lists what other containers (dependencies) it needs from the outside world. When you depend on an external library, such as rand for random numbers, you’re telling Cargo "fetch the package published under this name from crates.io, and let my crate use the library crate inside it." Cargo resolves the whole dependency graph — your crate’s dependencies, their dependencies, and so on — downloads the needed versions, compiles each one as its own crate, and links them all into your binary. The exact versions actually used are locked into Cargo.lock, so a build stays reproducible even after a dependency publishes a newer version.
For larger projects, Cargo also supports workspaces: a set of related packages that share one Cargo.lock and one target/ output directory, declared with a [workspace] table in a root Cargo.toml. Workspaces are how large real-world Rust codebases — a web server with a core library, a CLI, and internal tools — stay organized as several packages instead of one enormous one.
Syntax
The general shape of a package on disk is defined by its Cargo.toml manifest:
[package]
name = "my_project"
version = "0.1.0"
edition = "2021"
[dependencies]
rand = "0.8"
[package]— metadata: the crate/package name, its version (following semantic versioning), and which Rust edition to compile against.[dependencies]— every external package this crate needs, with a version requirement string ("0.8"means "the latest 0.8.x release compatible by semver").edition— selects language-edition rules (2015, 2018, or 2021); this course targets 2021 throughout.
And the conventional directory layout Cargo expects:
my_project/
├── Cargo.toml the package manifest
├── Cargo.lock exact resolved dependency versions (auto-generated)
└── src/
├── main.rs crate root of the binary crate (if any)
├── lib.rs crate root of the library crate (if any)
└── bin/
└── extra_tool.rs an additional, separate binary crate
Once a dependency is declared, you bring its items into scope the same way as your own modules or the standard library, with a use path rooted at the crate name:
| Path form | Meaning |
|---|---|
use std::collections::HashMap; |
Bring an item from the standard library crate into scope. |
use rand::Rng; |
Bring a trait from an external crate (declared in Cargo.toml) into scope. |
use crate::utils::square; |
An absolute path starting from your own crate’s root. |
pub use inner::Thing; |
Re-export an item so it becomes part of your own crate’s public API. |
Examples
Example 1: Organizing Code Into Modules Inside a Crate
Every crate, no matter how small, has an internal module tree. Here the crate root defines two nested modules and calls their public functions:
mod front_of_house {
pub mod hosting {
pub fn greet_customer(name: &str) -> String {
format!("Welcome, {}!", name)
}
}
pub mod serving {
pub fn take_order(dish: &str) -> String {
format!("Order received: {}", dish)
}
}
}
use front_of_house::hosting::greet_customer;
use front_of_house::serving::take_order;
fn main() {
let greeting = greet_customer("Alice");
let order = take_order("Pasta Primavera");
println!("{}", greeting);
println!("{}", order);
}
Output:
Welcome, Alice!
Order received: Pasta Primavera
hosting and serving are marked pub mod so code outside front_of_house (here, the crate root itself) can reach into them, and their functions are marked pub fn for the same reason. This is exactly the kind of structure a library crate’s src/lib.rs would use to organize a real public API.
Example 2: Flattening a Public API With pub use
Consumers of a crate shouldn’t have to know its internal module layout. pub use re-exports an item at a shallower path:
mod utils {
pub mod math {
pub fn square(n: i32) -> i32 {
n * n
}
}
}
pub use utils::math::square;
fn main() {
let result = square(6);
println!("6 squared is {}", result);
}
Output:
6 squared is 36
Without the pub use utils::math::square; line, callers would have to write the full path utils::math::square(6). In a real library crate, this pattern is how crates like serde let you write serde::Serialize instead of chasing internal submodules.
Example 3: The Standard Library Is a Crate Too
Because std is linked automatically, using its types requires no Cargo.toml entry at all — only a use path:
use std::collections::BTreeMap;
fn main() {
let mut scores: BTreeMap = BTreeMap::new();
scores.insert(String::from("Alice"), 90);
scores.insert(String::from("Bob"), 85);
scores.insert(String::from("Charlie"), 95);
for (name, score) in &scores {
println!("{}: {}", name, score);
}
}
Output:
Alice: 90
Bob: 85
Charlie: 95
BTreeMap keeps entries sorted by key, which is why the output is alphabetical and, unlike HashMap, deterministic every run — useful whenever you need predictable iteration order.
How It Works Step by Step
When you run cargo build (or cargo run) on a real package, this is what happens:
- Cargo reads
Cargo.tomlto find the package’s name, edition, and declared dependencies. - It resolves the full dependency graph — including dependencies of dependencies — picking versions that satisfy every semver constraint, and writes or reuses
Cargo.lockso the exact versions are pinned. - Each dependency is compiled as its own crate, producing an intermediate library artifact, before your own code is touched.
rustcis invoked on your crate root (src/main.rsorsrc/lib.rs). Starting there, it follows everymoddeclaration to pull in the rest of your source files, building one unified module tree for the whole crate.- Within that tree, the compiler enforces privacy (only
pubitems are visible outside the module that defines them), type-checks everything, and runs the borrow checker across the whole crate at once. - The compiled dependency crates are linked together with your crate’s compiled code into one binary, placed in
target/debug/ortarget/release/.
This is also why a bare rustc some_file.rs invocation — the same kind used to check the standalone examples in this lesson — only ever sees the standard library automatically; it has no Cargo.toml to consult, so it can’t resolve an external crate like rand without extra flags and a local copy of that crate. That’s the practical reason Cargo exists: it’s the layer that turns "a crate" into "a crate plus its resolved, reproducible dependency graph."
Common Mistakes
Mistake 1: Forgetting to Mark Items pub
Everything in Rust is private by default, visible only inside the module that defines it (and that module’s descendants). A common early mistake is defining a function inside a module and then calling it from outside without pub:
mod kitchen {
fn prepare_dish(dish: &str) -> String {
format!("{} is ready", dish)
}
}
fn main() {
let dish = kitchen::prepare_dish("Soup");
println!("{}", dish);
}
error[E0603]: function `prepare_dish` is private
--> src/main.rs:6:28
|
6 | let dish = kitchen::prepare_dish("Soup");
| ^^^^^^^^^^^^ private function
The fix is to add pub to the function (and to every module along the path, if it were nested deeper):
mod kitchen {
pub fn prepare_dish(dish: &str) -> String {
format!("{} is ready", dish)
}
}
fn main() {
let dish = kitchen::prepare_dish("Soup");
println!("{}", dish);
}
Soup is ready
Mistake 2: Using a Crate Without Declaring It as a Dependency
Writing a use line for an external crate isn’t enough by itself — the crate must be listed in Cargo.toml first, or Cargo has nothing to fetch and link:
[package]
name = "word_generator"
version = "0.1.0"
edition = "2021"
[dependencies]
use rand::Rng;
fn main() {
let mut rng = rand::thread_rng();
let n: u32 = rng.gen_range(1..100);
println!("{}", n);
}
error[E0432]: unresolved import `rand`
--> src/main.rs:1:5
|
1 | use rand::Rng;
| ^^^^ use of unresolved module or unlinked crate `rand`
The fix is to add the dependency to the manifest first, which lets Cargo resolve, download, and link it on the next build:
[dependencies]
rand = "0.8"
Mistake 3: Confusing Package, Crate, and Module Names
Because a package, its default library crate, and its top-level module are usually all named the same thing (taken from Cargo.toml‘s name field, with hyphens converted to underscores for the crate name), it’s easy to conflate them. The package is what you depend on in Cargo.toml (e.g. rand = "0.8"); the crate name is what you write after use (e.g. use rand::Rng;); and inside your own project, crate:: in a use path always means "the root of the crate currently being compiled," never the package’s name.
Best Practices
- Keep binaries thin: put real logic in a library crate (
src/lib.rs) and havesrc/main.rsmostly parse input and call into it — this makes the logic testable and reusable fromsrc/bin/tools too. - Use sensible semver ranges for dependencies in
Cargo.toml, and commitCargo.lockfor binary crates so builds stay reproducible (published library crates conventionally omit it). - Expose the smallest public API you can: keep helper modules private and use
pub useto re-export only the handful of items consumers actually need at the crate root. - Reach for a Cargo workspace once you have more than one related package, such as a core library plus a CLI, instead of duplicating dependency versions across separate projects.
- Run
cargo doc --openperiodically to see your crate’s public API the way a consumer would — it’s a fast way to notice you’ve accidentally exposed something internal. - Prefer well-established crates from crates.io over reinventing common functionality, but review new dependencies before adding them — every dependency is code you’re trusting to compile and run.
Practice Exercises
- Write a program with two modules,
mathandtext, each containing one public function (for examplemath::double(n: i32) -> i32andtext::shout(s: &str) -> Stringthat uppercases a string and adds an exclamation mark). Call both frommainand print the results. - Take the modules from exercise 1 and make only
math::doublereachable as a short name frommainby addingpub use math::double;at the top of the file, while leavingtext::shoutreachable only through its full path. Confirm both still work. - On paper (no compiler needed), sketch the
Cargo.tomland directory layout for a package namedweather_kitthat has one library crate with the core logic and two binary crates insrc/bin/:fetch.rsandreport.rs. Note which crate(s) are allowed to call into the library crate, and why.
Summary
- A crate is the smallest unit the compiler builds at once: a binary crate (has
fn main) or a library crate (nomain, exposes a public API). - A package is a directory with a
Cargo.tomlmanifest plus one or more crates; it can contain at most one library crate but many binary crates. - Compilation starts at the crate root (
src/main.rsorsrc/lib.rs) and followsmoddeclarations to build the full module tree. - Cargo reads
Cargo.toml, resolves the dependency graph against crates.io, locks exact versions inCargo.lock, and compiles and links every crate together. - The standard library (
std) is itself a crate, linked automatically, which is whyuse std::...never needs aCargo.tomlentry. - Privacy is enforced at compile time: mark items
pubto expose them outside their defining module, and usepub useto re-export a clean public API. - Workspaces let multiple related packages share one lockfile and build output for larger projects.
