Skip to Content
Packages, Crates and Modules

Packages and Crates

A package is a Cargo project described by Cargo.toml. A crate is a unit of Rust code compiled together. A module groups related items inside a crate and controls how other code can access them.

These concepts solve different problems. A package collects build settings and dependencies. A crate becomes an executable or a reusable library. Modules give that crate an internal structure. Understanding the boundaries explains why two files in the same project sometimes need different paths to call each other.

We will build a small personal reading tracker. First, it adds page counts in one executable. Then we share that calculation through a library, add another executable, and organize the library into modules with a clear public interface.

The examples use stable Rust and edition 2024, which requires Rust 1.85 or newer. All four stages and the exercise solutions were checked with Rust 1.88.0. Review Functions, Structs, and References and Borrowing if those concepts are new. Follow the stages in order: replace the named files when instructed. The final multi-file project is also complete on its own.

What is a package in Rust?

A package is the unit Cargo builds, checks, and distributes using a manifest named Cargo.toml. The manifest describes the package’s name, version, Rust edition, dependencies, and optional build settings.

A package contains at least one crate. It can contain one library crate at most and any number of binary crates. For example, our reading tracker can include shared tracking logic, an everyday command, and a separate goal-checking command in one package.

Use a package for a project you want Cargo to manage. If several independently versioned libraries need to live together, use separate packages, often grouped in a Cargo workspace. A workspace is a collection of packages; it is not another name for a crate.

What is a crate?

A crate is a compilation unit: Rust starts from one source file and compiles the code belonging to that crate together. That code may occupy one file or many module files.

The starting file is the crate root. Its contents form the crate’s root module. The word “root” describes where compilation and module paths begin, not a special directory you must create.

Binary crates vs library crates

Crate kindPurposeConventional rootHow you use it
BinaryProduce an executable, such as a command-line tool or server.src/main.rsRun it; an ordinary executable starts in fn main().
LibraryProvide code that other crates can call.src/lib.rsImport its exposed items; it needs no main entry point.

Choose a binary when users need to run a program. Choose a library when code needs to be shared or called independently of one program’s entry point. A project can benefit from both: keep reusable calculations in the library and terminal interaction in the binary.

Creating src/main.rs and src/lib.rs gives the package two crates. They do not merge into one just because they share a directory.

Create the reading tracker package

In a terminal, run:

cargo new reading-tracker --edition 2024 cd reading-tracker

Cargo creates Cargo.toml and src/main.rs, along with version-control files when applicable. Replace the generated manifest with:

Cargo.toml
[package] name = "reading-tracker" version = "0.1.0" edition = "2024" [dependencies]

This is TOML, a configuration format with named sections and key-value settings. name identifies the package, version records its release version, and edition selects a language edition. An edition is not the installed compiler’s version; one compiler can support several editions.

Our dependency section is empty because the standard library provides everything we need. In a larger project, this section declares external packages Cargo should resolve and build.

Cargo conventionally discovers src/main.rs and src/lib.rs; you do not need to list them here. Custom layouts can use [lib] and [[bin]] settings, but conventional paths keep a beginner project easy to follow.

Stage 1: one binary crate

Replace the generated program:

src/main.rs
fn main() { let reading_sessions = [24_u32, 18]; let total: u32 = reading_sessions.iter().sum(); println!("Pages read today: {total}"); }

Run:

cargo run

Program output, apart from Cargo’s build messages:

Pages read today: 42

Each number represents pages read in one session. iter().sum() adds them. The u32 type is an unsigned integer; these small page counts fit comfortably in it.

Cargo gives the discovered executable the package name, reading-tracker. After a build, target/ contains generated artifacts. Cargo also creates Cargo.lock to record dependency resolution; it is different from the editable package manifest.

Share logic with a library crate

Stage 2 builds on Stage 1. Add src/lib.rs and replace src/main.rs:

src/lib.rs
pub fn total_pages(sessions: &[u32]) -> u32 { sessions.iter().sum() }
src/main.rs
use reading_tracker::total_pages; fn main() { let reading_sessions = [24, 18]; println!("Pages read today: {}", total_pages(&reading_sessions)); }

Run cargo run --bin reading-tracker. The output is still Pages read today: 42.

The library accepts a borrowed slice, &[u32], so callers can supply page counts without transferring ownership. pub exposes the function for other crates to call. We will examine its visibility rules in the next part.

use introduces a shorter name for an item. The path begins with reading_tracker, the library’s default crate name. Cargo converts the hyphen in reading-tracker to an underscore for this Rust identifier.

The binary consumes the library’s interface. This division lets another executable reuse the calculation instead of copying it.

Add a second binary crate

Create the src/bin directory and this file:

mkdir -p src/bin
src/bin/goal.rs
use reading_tracker::total_pages; fn main() { let total = total_pages(&[24, 18]); let remaining = 30_u32.saturating_sub(total); println!("Pages remaining: {remaining}"); }
cargo run --bin goal

Output:

Pages remaining: 0

saturating_sub stops at zero when the total exceeds the goal. An unsigned page count cannot represent a negative remainder.

The package now contains three crates: the library, the reading-tracker binary, and the goal binary. Both binaries use the same library. Once several binaries exist, select one with --bin; plain cargo run needs a selection unless a default is configured.

The official package and crate reference  explains the underlying distinction. For other target layouts, see Cargo targets .

Control Scope and Privacy with Modules

A module is a named group of Rust items, such as functions, structs, constants, and other modules. Modules create paths to those items and help control which parts of an implementation callers can use.

Scope describes where a name is available. Visibility describes whether code is allowed to access an item. A use declaration changes the names available in its scope; it does not grant permission to access a private item.

Modules help a growing reading tracker separate book data, progress calculations, report formatting, and eventually storage. They also let us change a formatting helper without making every caller depend on it.

Declare modules with mod

Stage 3 replaces only src/lib.rs and src/main.rs. Keep the Stage 2 manifest and src/bin/goal.rs.

src/lib.rs
pub mod catalog { pub fn total_pages(sessions: &[u32]) -> u32 { sessions.iter().sum() } } pub mod report { const GOAL_PAGES: u32 = 30; pub fn summary(sessions: &[u32]) -> String { let total = crate::catalog::total_pages(sessions); self::formatting::line(total) } mod formatting { pub(super) fn line(total: u32) -> String { let remaining = super::GOAL_PAGES.saturating_sub(total); format!("Read {total} pages; {remaining} pages left toward today's goal.") } } } pub use self::catalog::total_pages;
src/main.rs
use reading_tracker::report; fn main() { println!("{}", report::summary(&[24, 18])); }

Run cargo run --bin reading-tracker:

Read 42 pages; 0 pages left toward today's goal.

mod catalog { ... } defines a module inline. pub mod catalog also makes that module available outside the library. report handles the displayed result, while its private formatting child handles the wording.

The final pub use is a re-export: it exposes the existing function as reading_tracker::total_pages. This preserves the Stage 2 goal binary’s import without creating another function or another crate.

Read the module tree

The library’s structure is:

crate (the library root in src/lib.rs) ├── catalog │ └── total_pages └── report ├── GOAL_PAGES ├── summary └── formatting └── line

catalog and report are siblings because they have the same parent. formatting is a child of report; report is its parent. Every crate has its own tree, so the two binaries have separate roots even though this diagram shows only the library.

Modules are logical boundaries, not necessarily files. This whole tree currently lives in one file.

Rust paths: crate, self, and super

A path names an item by following segments separated with ::.

Path in the library exampleStarting pointMeaning
crate::catalog::total_pagesCurrent crate’s rootFind the calculation from anywhere inside this library.
self::formatting::line inside reportCurrent moduleEnter report’s formatting child.
super::GOAL_PAGES inside formattingParent moduleRead the constant defined in report.
reading_tracker::report::summary inside a binaryNamed library crateCall the library from a different crate.

An absolute path has a fixed starting point, such as crate:: for the current crate or a dependency’s crate name for another crate. A relative path starts from the current context, such as self:: or super::. Repeated super::super:: segments can climb further up the tree.

Inside the binary, report::summary works because use reading_tracker::report brings report into scope. That shorter path refers to the imported name.

In src/main.rs, crate:: means the binary’s root. It does not point to src/lib.rs. Use reading_tracker:: to reach this package’s library. In edition 2024, a leading :: also does not mean your own root module; it starts external-crate lookup.

The path keyword self means the current module here. It has a different role from a method’s self parameter, which refers to the value receiving a method call.

Private items and the pub keyword

Ordinary module items are private by default. A private item is accessible within the module where it is defined and that module’s descendants. Its parent cannot directly access a private item defined inside a child.

In our example, the formatting child can read its parent’s private GOAL_PAGES. The reverse access needs permission: pub(super) makes line visible to its parent module and that parent’s descendants.

Useful visibility choices include:

DeclarationIntended access
fn helper()The defining module and its descendants.
pub(super) fn helper()The parent module and its descendants.
pub(crate) fn helper()Anywhere in the current crate.
pub fn helper()Public, provided the path through its ancestor modules is accessible.

pub mod report does not automatically expose its functions. Conversely, pub fn summary inside a private module does not automatically expose that module to external callers. Each part of an external path must be accessible, unless a public re-export supplies another accessible path.

Privacy follows crate boundaries, even inside one package. Changing a library function to pub(crate) prevents the package’s binaries from calling it. Use pub for the library interface that those binaries need.

Trigger a private-function error and fix it

In the Stage 3 library, temporarily change pub fn summary to fn summary. Leave the binary unchanged and run:

cargo check --bin reading-tracker

This is an intentional failing experiment. The diagnostic includes:

error[E0603]: function `summary` is private

The function still exists, and the path still identifies it. The binary lacks permission to call it. Restore pub fn summary, then run the check again. It succeeds because both report and summary are public.

Organize modules across files

mod catalog; declares a module whose contents are loaded from another file. At the crate root, Rust looks for src/catalog.rs or src/catalog/mod.rs. Choose one layout for that module; creating both causes an ambiguity error.

Inside src/report.rs, mod formatting; loads src/report/formatting.rs or src/report/formatting/mod.rs. The file hierarchy follows where the declaring module belongs.

Creating a .rs file alone does not include it. A mod declaration attaches the module to the tree. A use declaration supplies a name for something already available; it does not load a file.

Complete multi-file reading tracker

Stage 4 replaces the three existing Rust files and adds the three module files below. Keep the same Cargo.toml. To start here independently, run the earlier cargo new command and use that manifest.

The tracker now stores a title and page count for each book. It keeps those fields private and provides methods for recording and reading progress.

From the package directory, create the folders needed for the binary and child module:

mkdir -p src/bin src/report
reading-tracker/ ├── Cargo.toml └── src/ ├── lib.rs ├── main.rs ├── catalog.rs ├── report.rs ├── report/ │ └── formatting.rs └── bin/ └── goal.rs

The library root and book data

src/lib.rs
mod catalog; pub mod report; pub use self::catalog::Book; #[cfg(test)] mod tests { use super::{Book, report}; #[test] fn records_multiple_sessions() { let mut book = Book::new("Notes on the Night Sky"); book.record_pages(10); book.record_pages(14); assert_eq!(book.title(), "Notes on the Night Sky"); assert_eq!(book.pages_read(), 24); } #[test] fn empty_catalog_keeps_the_full_goal() { assert_eq!( report::summary(&[]), "Read 0 pages; 30 pages left toward today's goal." ); } #[test] fn combines_books_and_stops_remaining_at_zero() { let mut first = Book::new("Notes on the Night Sky"); let mut second = Book::new("A Walk Along the Coast"); first.record_pages(24); second.record_pages(18); assert_eq!( report::summary(&[first, second]), "Read 42 pages; 0 pages left toward today's goal." ); } }
src/catalog.rs
pub struct Book { title: String, pages_read: u32, } impl Book { pub fn new(title: &str) -> Self { Self { title: title.to_owned(), pages_read: 0, } } pub fn record_pages(&mut self, pages: u32) { self.pages_read += pages; } pub fn title(&self) -> &str { &self.title } pub fn pages_read(&self) -> u32 { self.pages_read } } pub(crate) fn total_pages(books: &[Book]) -> u32 { books.iter().map(Book::pages_read).sum() }

catalog is now a private implementation module. The public re-export makes its public Book type available as reading_tracker::Book, without exposing reading_tracker::catalog to binaries.

Even a public struct’s fields are private unless explicitly marked public. Callers use Book::new, record_pages, and the getter methods instead of assigning fields directly. total_pages is internal to the library, so pub(crate) is sufficient.

Reports and their formatting child

src/report.rs
mod formatting; const GOAL_PAGES: u32 = 30; pub fn summary(books: &[crate::Book]) -> String { let total = crate::catalog::total_pages(books); self::formatting::line(total) }
src/report/formatting.rs
pub(super) fn line(total: u32) -> String { let remaining = super::GOAL_PAGES.saturating_sub(total); format!("Read {total} pages; {remaining} pages left toward today's goal.") }

These paths work just as they did inline. Moving the module bodies into declared files changes their storage, not their position in the module tree or their visibility rules.

Two binaries using one library

src/main.rs
use reading_tracker::{Book, report}; fn main() { let mut first = Book::new("Notes on the Night Sky"); let mut second = Book::new("A Walk Along the Coast"); first.record_pages(24); second.record_pages(18); let books = [first, second]; for book in &books { println!("{}: {} pages", book.title(), book.pages_read()); } println!("{}", report::summary(&books)); }
src/bin/goal.rs
use reading_tracker::{Book, report}; fn main() { let mut book = Book::new("Morning Essays"); book.record_pages(12); println!("{}", report::summary(&[book])); }

Check and run the project:

cargo check --all-targets cargo test cargo run --bin reading-tracker cargo run --bin goal

The main binary prints:

Notes on the Night Sky: 24 pages A Walk Along the Coast: 18 pages Read 42 pages; 0 pages left toward today's goal.

The goal binary prints:

Read 12 pages; 18 pages left toward today's goal.

cargo test runs three library tests. #[cfg(test)] includes the test module during test compilation; #[test] marks each test function. The tests check accumulated sessions, an empty catalog, and totals beyond the goal.

This teaching app tracks one day’s progress in memory. It does not persist records or reset them at midnight. Its u32 arithmetic assumes modest page counts; a production tracker should handle overflow and validate inputs according to its requirements.

Package vs crate vs module

ConceptWhat it representsWhat defines itReading tracker example
PackageA Cargo-managed project containing one or more crates.Cargo.toml with a [package] section.reading-tracker and its build settings.
CrateOne separately compiled binary or library.A crate root, discovered or configured by Cargo.The library, main command, and goal command.
ModuleA group of items and a privacy boundary within a crate.mod name { ... } or mod name;.catalog, report, and report::formatting.

How the three work together

Cargo reads the package manifest, discovers each target, and builds its crate. Compilation starts at that crate’s root and follows its module declarations. Code then reaches permitted items through paths or imported names.

For our main command, Cargo builds the library and makes it available to the binary. The binary calls the public report::summary function. That function uses an internal calculation and private formatting module. The caller gets a result without depending on those details.

In a larger application, keep modules focused on responsibilities such as storage, reporting, or validation. Use separate crates when you need independently reusable code or another executable. Avoid creating a module for every tiny function; boundaries should help readers find related work.

Common beginner mistakes

ProblemWhy it happensCorrection
E0583: module file not foundmod catalog; has no file at the expected location.Match the declaration to src/catalog.rs or src/catalog/mod.rs.
E0761: two files for one moduleBoth supported file layouts exist.Keep one layout for that module.
E0603: private module or functionA path crosses a visibility boundary.Expose the intended interface with pub or a public re-export.
E0432 or E0433: unresolved import or pathA name is misspelled, undeclared, or starts at the wrong crate.Check mod declarations and use the library name from binaries.
E0451: private struct fieldsA caller constructs Book by naming its private fields.Use Book::new and its methods.
Cargo cannot choose a binarySeveral binaries exist and no default was selected.Pass --bin reading-tracker or --bin goal.

Do not put mod catalog; in each binary to reuse the library’s module. That declares a module in each binary’s own tree, rather than importing the library. Declare shared modules in src/lib.rs and import the public library interface.

When fixing a privacy error, first decide whether the caller should have access. Exposing a narrow operation is often better than making all implementation details public.

Practical exercises

  1. Change the goal to 50 pages. Update the final project’s goal constant and its tests. The main command should report 8 pages remaining; the goal command should report 38. Solution: edit GOAL_PAGES in src/report.rs, update both affected summary expectations, and rerun cargo test.
  2. Add a book-count report. Keep the final goal at 30. Add a public function in report that accepts &[crate::Book] and returns the number of books. Solution: return books.len() with return type usize. Add tests for an empty slice and a one-book slice, then call it from the main binary.
  3. Create a third executable. Add src/bin/review.rs that records 9 and 16 pages in two sessions for one book. Solution: import Book and report from reading_tracker, call record_pages twice, and print report::summary(&[book]). Run cargo run --bin review; it should report 25 pages read and 5 remaining. It needs no new library module or dependency.

Frequently asked questions

Is a Rust package the same as a crate?

No. A package is managed through a Cargo manifest and can contain several crates. A crate is one compilation unit. A package containing only one crate can make the terms seem interchangeable, but the boundaries still differ.

Can a package contain both main.rs and lib.rs?

Yes. Cargo treats src/main.rs as a binary root and src/lib.rs as a library root. The binary calls the library through its crate name and public interface.

Can a Rust package have multiple libraries?

A Cargo package has at most one library target. Put independently reusable libraries in separate packages. A workspace can keep those packages together and coordinate common development tasks.

What is the difference between a crate and a module?

A crate is compiled as a unit and has its own root. A module organizes items within that crate. Moving a module into another source file does not make it a new crate.

Does pub mod make everything inside public?

No. It exposes the module, while its ordinary items remain private unless their declarations give them broader visibility. Public structs also keep their fields private by default.

Why does crate:: work in lib.rs but fail in main.rs?

Each file starts a different crate. A path beginning with crate:: stays in whichever crate contains that path. In the binary, use the library’s name, such as reading_tracker::Book.

Should I use report.rs or report/mod.rs?

Both layouts are supported. This tutorial uses report.rs with child files inside report/. Follow one consistent layout and never create both root files for the same module.

Key takeaways

  • A package supplies Cargo’s project settings; a crate supplies a compilation boundary.
  • Binary crates run programs, while library crates expose reusable code.
  • Each crate has its own root and module tree.
  • mod defines or loads modules; use introduces names for accessible items.
  • crate, self, and super choose different path starting points.
  • Visibility exposes deliberate interfaces. It does not change just because files share a package.

Next steps and official references

Practice the ownership rules behind moving books into arrays, review borrowing to understand the slice arguments, and use Enums when adding reading states such as planned, active, and finished. The Rust tutorial index links to the surrounding lessons.

The following official resources verify the language and Cargo rules. The reading tracker and its exercises are original examples created for this lesson.