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

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)) = 20

Mini-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)

Pro Tips

📋 Quick Reference — Cargo

CommandDoes
cargo new nameScaffold a new package
cargo runBuild then run the binary
cargo checkFast type-check, no binary
cargo testRun the test suite
cargo add crateAdd a dependency
cargo fmt / clippyFormat / lint your code

🎉 Lesson Complete!

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

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.