Cargo, Crates & the Module Workflow
Reviewed & published by Brayan K
Cargo is Rust's official build system and package manager — it creates projects, compiles your code, runs tests, manages dependencies, and ties together the crates and modules that make up every Rust program.
Part of the free Rust course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
In this lesson you'll learn the everyday Cargo commands, what lives in Cargo.toml, how crates differ from modules, how to add dependencies, and how cargo fmt and cargo clippy keep your code clean.
What You'll Learn in This Lesson
- cargo new, build, run, and test
- What Cargo.toml describes about your package
- Crates vs modules, and the :: path separator
- Adding dependencies (crates) to a project
- Organizing code with mod, pub, and use
- cargo fmt, cargo clippy, and the target directory
1️⃣ Creating & Running a Project
A Cargo package starts with one command. cargo new scaffolds a folder with a Cargo.toml manifest, a src/main.rs entry point, and a git repo with /target already ignored. From there you compile and run with a single command. These are commands you type in your terminal, not Rust code:
The src/main.rs that Cargo runs always starts at the main function. Here is a slightly fuller version of that file — this is the real Rust that cargo run would build and execute:
fn main() {
// A tiny program Cargo would compile and run for you.
let project = "hello_cargo";
let edition = 2021;
println!("Building {} (edition {})", project, edition);
// Cargo runs `main`, which can call other functions in the same crate.
let total = add(2, 40);
println!("2 + 40 = {}", total);
// The crate is the whole compilation unit; functions like add live inside it.
println!("Done. Cargo would put the binary in target/debug/{}", project);
}
fn add(a: i32, b: i32) -> i32 {
a + b
}2️⃣ Cargo.toml, Crates & Dependencies
The Cargo.toml manifest describes your package: its name, version, Rust edition, and the external crates it depends on. Each entry under [dependencies] is a crate downloaded from crates.io, Rust's package registry. A typical manifest looks like this:
You rarely edit dependencies by hand — cargo add rand inserts the line for you and resolves a compatible version. Cargo then records the exact version in Cargo.lock so builds are reproducible. A crate is the whole compilation unit; the standard library is itself a crate (std) that's always available. Inside a single crate, you organize code into modules:
// A crate can be split into MODULES to organize code.
mod geometry {
// `pub` makes an item visible outside its module.
pub fn area(width: f64, height: f64) -> f64 {
width * height
}
// Modules can nest. This inner module groups related helpers.
pub mod perimeter {
pub fn rectangle(width: f64, height: f64) -> f64 {
2.0 * (width + height)
}
}
}
fn main() {
// Reach into a module with the `::` path separator.
let a = geometry::area(3.0, 4.0);
let p = geometry::perimeter::rectangle(3.0, 4.0);
println!("area = {}", a);
println!("perimeter = {}", p);
}Items are private by default; pub exposes them. You reach into a module with the :: path separator — the same syntax you use to reach into an external crate (e.g. rand::random).
3️⃣ The use Keyword & Keeping Code Clean
Typing full paths everywhere gets noisy. The use keyword brings a path into scope so you can call it by its short name. You can import several items at once with braces:
mod greetings {
pub fn hello(name: &str) -> String {
format!("Hello, {}!", name)
}
pub fn bye(name: &str) -> String {
format!("Goodbye, {}!", name)
}
}
// `use` brings paths into scope so you can call them by short name.
use greetings::{hello, bye};
fn main() {
println!("{}", hello("Cargo"));
println!("{}", bye("Cargo"));
}Once your code grows, two commands keep it tidy. Run them in your terminal:
Your turn. Fill in the blanks marked ___, then run it.
// 🎯 YOUR TURN — fill in the blanks so the module compiles and runs.
mod math {
// 1) Make double visible OUTSIDE the module.
___ fn double(n: i32) -> i32 {
n * 2
}
}
// 2) Bring double into scope by its path.
use math::___;
fn main() {
let result = double(21);
println!("double(21) = {}", result);
println!("double(double(5)) = {}", double(double(5)));
}
// ✅ Expected output:
// double(21) = 42
// double(double(5)) = 20Mini-Challenge: A Temperature Module
Organize a small conversion into its own module, import it with use, and loop over an array. Run it with cargo run and check the output.
// 🎯 MINI-CHALLENGE: build a tiny temperature module.
//
// 1. Create a module `temperature` with a PUBLIC function
// c_to_f(c: f64) -> f64 that returns c * 9.0 / 5.0 + 32.0.
// 2. `use` it in main.
// 3. Loop over the array [0.0, 25.0, 100.0] and print each conversion.
//
// ✅ Example output:
// 0C = 32F
// 25C = 77F
// 100C = 212F
fn main() {
// your code here
}Common Errors (and the fix)
- ❌ "function is private" — You called a module function without marking it pub. ✅ Add pub in front of fn (and in front of mod for nested modules).
- ❌ "could not find `X` in the crate root" — A bad use path or a missing mod declaration. ✅ Check the full path and that the module is declared.
- ❌ "no matching package found" — A dependency name or version in Cargo.toml is wrong. ✅ Use cargo add <crate> so Cargo fills in a valid version.
- ❌ Committing target/ — Build output bloats your repo. ✅ The default .gitignore already excludes it; never commit generated artifacts.
Pro Tips
- 💡 Use cargo check while coding: it catches type errors fast without the slower code-generation step of a full build.
- 💡 Let Cargo manage versions: prefer cargo add over hand-editing Cargo.toml, and commit Cargo.lock for binaries.
- 💡 Split big files: move a module into its own file with mod name; and a matching name.rs next to it.
📋 Quick Reference — Cargo
| Command | Does |
|---|---|
| cargo new name | Scaffold a new package |
| cargo run | Build then run the binary |
| cargo check | Fast type-check, no binary |
| cargo test | Run the test suite |
| cargo add crate | Add a dependency |
| cargo fmt / clippy | Format / lint your code |
🎉 Lesson Complete!
- ✅ cargo new / build / run / check / test drive the whole workflow
- ✅ Cargo.toml declares your package and its dependency crates
- ✅ A crate is the compilation unit; modules (mod/pub/use) organize code inside it
- ✅ cargo fmt formats, cargo clippy lints, and target/ holds build output (never commit it)
Practice quiz
What is the difference between a crate and a module?
- They are the same thing
- A module is the compilation unit; a crate is inside it
- A crate is the compilation unit; a module organizes code inside a crate
- Modules are only for tests
Answer: A crate is the compilation unit; a module organizes code inside a crate. A crate is what Cargo builds; modules are namespaces that organize code within a crate.
Which command compiles AND runs your binary in one step?
- cargo run
- cargo check
- cargo build
- cargo new
Answer: cargo run. cargo run compiles the crate (and dependencies) then executes the resulting binary.
What does cargo check do that cargo build does not?
- Runs the tests
- Publishes the crate
- Formats the code
- Type-checks quickly without producing a binary
Answer: Type-checks quickly without producing a binary. cargo check skips code generation, so it type-checks much faster than a full build.
By default, items in a module are visible from outside it...
- Always
- Never — they are private unless marked pub
- Only inside tests
- Only if named in UPPERCASE
Answer: Never — they are private unless marked pub. Items are private by default; pub exposes them outside their module.
Which path separator reaches into a module or external crate?
- ::
- .
- ->
- /
Answer: ::. Rust uses the :: path separator, e.g. geometry::area or rand::random.
What does this print? mod math { pub fn double(n: i32) -> i32 { n * 2 } } use math::double; fn main() { println!("{}", double(double(5))); }
- 10
- 25
- 20
- It does not compile
Answer: 20. double(5) is 10, and double(10) is 20.
What does the use keyword do?
- Declares a new module
- Brings a path into scope so you can call it by its short name
- Downloads a crate
- Runs the tests
Answer: Brings a path into scope so you can call it by its short name. use imports a path into scope so you avoid typing full paths everywhere.
Where does Cargo declare a package's dependencies?
- src/main.rs
- Cargo.lock
- target/debug
- Cargo.toml under [dependencies]
Answer: Cargo.toml under [dependencies]. Dependencies are listed under [dependencies] in the Cargo.toml manifest.
Should you commit the target/ directory to version control?
- Yes, always
- No — it is generated build output and is git-ignored by default
- Only the release build
- Only Cargo.lock inside it
Answer: No — it is generated build output and is git-ignored by default. target/ holds reproducible generated artifacts; cargo new git-ignores it for you.
What do cargo fmt and cargo clippy do, respectively?
- Run tests and build
- Publish and download
- Format code and lint for mistakes/idioms
- Both format code
Answer: Format code and lint for mistakes/idioms. cargo fmt formats to the canonical style; cargo clippy lints for bugs and non-idiomatic code.
Continue this course
- Previous: Variables & Mutability
- Next: Data Types — Integers, floats, booleans, chars, tuples, and arrays in a statically typed language
- Quick reference: Rust cheat sheet
Frequently asked questions
What's the difference between a crate and a module?
A crate is the unit of compilation in Rust — it's what Cargo actually builds into a binary or a library. A module is a way to organize code inside a crate into named, nestable namespaces with controlled visibility. Put simply: one crate can contain many modules, and a project (package) can contain one library crate plus several binary crates. You declare modules with the mod keyword and pull external crates in as dependencies in Cargo.toml; you navigate both with the :: path separator.
Why do I run cargo run instead of rustc directly?
You can call rustc by hand, but Cargo is Rust's build system and package manager, and it removes almost all the tedium. cargo run compiles your crate (downloading and building any dependencies first), links everything, and then executes the resulting binary — all in one command. Cargo also manages your Cargo.toml manifest, locks exact dependency versions in Cargo.lock, runs your tests with cargo test, and formats and lints your code. For anything beyond a single throwaway file, Cargo is the standard workflow.
What is the target directory and should I commit it?
When you build, Cargo writes all compiled artifacts into a target/ directory — an unoptimized build under target/debug and an optimized one under target/release when you pass --release. These files are generated output, can be large, and are fully reproducible from your source, so you should never commit them. A new project created with cargo new already includes a .gitignore that excludes /target. If a build ever gets into a weird state, cargo clean deletes target/ so the next build starts fresh.
What do cargo fmt and cargo clippy do?
cargo fmt runs rustfmt, the official formatter, and rewrites your code into the canonical Rust style — consistent indentation, spacing, and line breaks — so every Rust project reads the same way. cargo clippy runs Clippy, a linter with hundreds of extra checks that catch common mistakes, non-idiomatic patterns, and easy performance wins that the compiler itself doesn't flag. Running both before you commit is a widely followed habit that keeps code clean and catches problems early.