Organize Code with Modules and Crates

RustBeginner
Practice Now

Introduction

As a Rust program grows, keeping every type and function in one file makes responsibilities harder to see. You have already seen a library and binary in the CLI Lab and a test module in the testing Lab. This Lab now connects those prepared examples into one model: a package is the project Cargo builds, crates are compilation units, and modules arrange names within a crate.

You will organize a small pantry-report package. The files and most implementation code are prepared, so each step focuses on one boundary: sharing from the library crate, declaring a file module, and importing a small public API into the binary crate.

Share a Function Between Crates

In this step, you will identify the package's two crates and make one library function available to the binary.

Move into the prepared Cargo package:

cd /home/labex/project/pantry-report

Cargo.toml describes one package named pantry-report. Cargo recognizes src/lib.rs as the root of a library crate and src/main.rs as the root of a binary crate. They belong to the same package but compile as separate crates.

Inspect the short crate roots. The sed -n commands print the requested line ranges without editing the files:

sed -n '1,120p' src/lib.rs
sed -n '1,120p' src/main.rs

The binary imports report_title through the library crate path. A package name containing a hyphen becomes a crate name containing an underscore in Rust source, so pantry-report becomes pantry_report.

The prepared return type &'static str describes a borrowed string literal that remains valid for the whole program. The 'static marker is a lifetime annotation. Explicit lifetime rules are intentionally outside this beginner course; no learner edit in this Lab depends on understanding or writing one.

Items are private to their module unless marked pub. Open the library root:

nano src/lib.rs

Change only the function declaration from:

fn report_title() -> &'static str {

to:

pub fn report_title() -> &'static str {

pub makes the function part of the library crate's public interface. Save with Ctrl+O, press Enter, and exit with Ctrl+X.

Use cargo check to type-check both crates without producing a final runnable build:

cargo check

Then run the binary crate:

cargo run --quiet

The output is:

Pantry Report

This proves the binary can cross the crate boundary and call the public library function.

Declare a File Module

In this step, you will connect the prepared inventory.rs file to the library crate's module tree.

A Rust source file is not compiled merely because it exists. The crate root must declare its module. For a declaration named inventory, Rust looks for src/inventory.rs and places its items under the path inventory::....

Inspect the prepared module file. As before, sed -n '1,200p' uses -n to suppress automatic output and 1,200p to print only the requested line range:

sed -n '1,200p' src/inventory.rs

The #[derive(Debug)] line asks Rust to generate standard debug-formatting support for Item. The visible program does not rely on that feature, so treat it as prepared metadata rather than a new requirement. The Item type, its fields, and describe are public. The helper availability has no pub, so it remains private inside the module. describe can call that private helper, while outside callers receive only the public result.

Open the library root:

nano src/lib.rs

Replace // MODULE_DECLARATION with:

pub mod inventory;

The first pub exposes the module through the library crate. The mod inventory; part attaches the file to the module tree. Save and exit Nano, then check the package:

cargo check
Finished `dev` profile ...

The successful check proves Rust found src/inventory.rs and compiled it as pantry_report::inventory.

Import and Use the Public API

In this step, you will use a use declaration to bring module items into the binary crate and print a pantry record.

Without an import, the full paths are pantry_report::inventory::Item and pantry_report::inventory::describe. A use declaration creates shorter names in the current scope without moving or copying the items.

Open the binary source:

nano src/main.rs

Replace // INVENTORY_IMPORT with this grouped import:

use pantry_report::inventory::{describe, Item};

Braces group two items that share the same path prefix. Next, replace // INVENTORY_REPORT with:

    let lentils = Item {
        name: String::from("lentils"),
        quantity: 4,
    };
    println!("{}", describe(&lentils));

The binary can construct Item because the type and both fields are public. It borrows the item for describe; that public function privately calls availability inside its own module.

Save and exit Nano, then run the package:

cargo run --quiet
Pantry Report
lentils: 4 jars (stocked)

The two lines show both crate-level sharing and module-level organization working through a small public API.

Summary

You worked with one Cargo package containing library and binary crates, made a library item public, declared a file module, followed crate and module paths, shortened those paths with use, and kept an implementation helper private behind a small public API.