Anatomy of a Cargo Project: Cargo and Modules
Chapters 4–7 had you reading code at the level of functions and types. This chapter zooms out to the project itself: what it's made of, who drives compilation, and where dependencies come from and go. By the end, you'll be able to:
- take an unfamiliar Cargo project (say, a fresh clone of jackdaw) and, within half a minute, point to the entry point, the dependency declarations, the tests, and the build artifacts;
- explain
tokio = { version = "1", features = ["full"] }precisely: how wide a version range that permits, and what itsfeaturesswitches control; - reach for the right arbiter the moment the AI hands back a diff —
check,test,clippy, andfmteach answer a different question.
The Big Picture: What Every File Does
Anatomy starts with the file list. Here is jackdaw's actual tree:
jackdaw/Cargo.lock
jackdaw/Cargo.toml
jackdaw/README.md
jackdaw/jackdaw.example.toml
jackdaw/src/ask.rs
jackdaw/src/cli.rs
jackdaw/src/config.rs
jackdaw/src/gateway.rs
jackdaw/src/lib.rs
jackdaw/src/main.rs
jackdaw/src/model.rs
jackdaw/src/openai.rs
jackdaw/src/provider.rs
jackdaw/src/router.rs
jackdaw/tests/integration.rsHow to read it: sort the tree into five groups. Cargo.toml is the project's manifest — metadata plus dependency declarations. Cargo.lock is the lockfile (Section 5). Under src/, every .rs file except the two special entry points, main.rs and lib.rs, is one functional module; tests/integration.rs holds integration tests (Chapter 15 unpacks those). The last group is for humans: README.md describes the project, and jackdaw.example.toml is a config template — copy it to jackdaw.toml and edit to taste. No target/ in the tree — .gitignore excludes it, because it's pure build output: delete it and Cargo rebuilds it, so it never enters version control.
Cargo.toml Line by Line: How the Project Introduces Itself
Cargo is build tool and package manager in one, and the manifest is the first file it reads. Start with the [package] section:
[package]
name = "jackdaw"
version = "0.1.0"
edition = "2024"
license = "MIT"
description = "A tiny, transparent, OpenAI-compatible LLM gateway with routing groups"How to read it: all five lines are metadata. name fixes both the binary's name and the library's external name. version is jackdaw's own version. edition = "2024" picks the language edition — which generation of language rules compiles your code — and that axis is orthogonal to how new your compiler is. license and description mainly matter to tooling at publish time.
Next, the dependencies:
# Dependencies are kept to the minimum the blueprint lists: nothing here is
# "nice to have" — each crate appears because a spec behavior needs it.
[dependencies]
tokio = { version = "1", features = ["full"] }
axum = "0.8"
reqwest = { version = "0.12", features = ["json", "stream"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
toml = "0.9"
clap = { version = "4", features = ["derive"] }
thiserror = "2"
anyhow = "1"
# ...How to read it: dependencies come in exactly two shapes — axum = "0.8" is shorthand for axum = { version = "0.8" }; you need the table form once you turn on a feature. Each line answers three questions: which crate, how new a version it may be, and which optional capabilities you want. The comment just above [dependencies] models a habit worth stealing: no "might as well" dependencies — every crate is there because a spec behavior needs it.
Last, the release settings:
# Shipping profile: small binaries beat last-percent compile speed for a
# teaching project you build once and copy around.
[profile.release]
lto = true
strip = trueHow to read it: [profile.release] affects only --release builds. lto turns on cross-crate link-time optimization; strip drops the symbol table. Both trade compile time for a smaller binary — and the comment above says outright that this is the right trade for a teaching project you build once and copy around.
main.rs and lib.rs: One Package, Two Crates
Three words to keep apart: a package is the unit of building and publishing; a crate is the unit of compilation; a module layers code inside a crate. One package may hold two crates: a binary crate (entry point src/main.rs) and a library crate (entry point src/lib.rs). jackdaw has both, on purpose: all the reusable logic lives in the library, and main.rs only wires the binary edge.
// ...
use jackdaw::cli::Cli;
use jackdaw::config::{self, ConfigError};
#[tokio::main]
async fn main() {
init_tracing();
if let Err(error) = run(Cli::parse()).await {
// `{:#}` prints the whole anyhow chain, one cause per line.
eprintln!("jackdaw: {error:#}");
std::process::exit(exit_code(&error));
}
}
// ...How to read it: start with these two use lines — the prefix is jackdaw::, not crate::. main.rs consumes its own library the way an outside customer would. main does exactly three things: set up logging, hand the command line to run, and translate the final error into an exit code. Cli::parse() is clap's subcommand parsing underneath (Chapter 10 unpacks that); .await you met in Chapter 7 — a yield point.
So where does the path jackdaw::cli come from? Files under src/ do not become modules automatically; lib.rs must declare each one:
// Teaching illustration: a module declaration looks like this
// (mechanism demo, not an excerpt from the source)
pub mod cli;
pub mod config;
// ...A single line, pub mod cli;, attaches src/cli.rs to the module tree and makes it public. Now reread use jackdaw::config::{self, ConfigError} in main.rs: {self, ConfigError} means "import the config module itself, plus the ConfigError type inside it."
INFO
When several independent packages share one target/ and one lockfile, the root manifest grows a [workspace] section. Workspaces are common in multi-package repositories; jackdaw is a single package and doesn't need one.
cargo Subcommands: One Question Each
Chapter 3 called the cargo commands the arbiter between you and the AI; arbitrating well means knowing what each command asks:
| Command | Question it answers |
|---|---|
cargo check | Does it compile? (no binary produced, fastest) |
cargo test | Does it behave? |
cargo clippy | Is it idiomatic? (catches what compiling alone won't) |
cargo fmt | Is the formatting uniform? (touches layout, not semantics) |
cargo build | Can it produce a usable binary? |
cargo run | What does it do when run right now? |
When the AI hands back a diff, the usual order is check → test → clippy: first "does it compile", then "does it behave", last "is it well written". To wrap up, jackdaw/README.md gives just two commands:
cargo test
cargo install --path . # or: cargo build --releaseHow to read it: cargo install --path . builds the release version and installs it into ~/.cargo/bin. Artifacts split by profile: everyday cargo run lands in target/debug/ (fast compiles, debug info); release builds land in target/release/ (optimized, and picking up the two switches from Section 2). Delete all of target/ whenever you like; Cargo rebuilds it.
Versions, the Lockfile, and Features
Semver reads as major.minor.patch, like 1.6.3. The "1" in the manifest is shorthand for ^1: anything ≥1.0.0 and <2.0.0. One rule that bites: when the major version is 0, the minor version steps into the major's role — axum's "0.8" permits only ≥0.8.0 and <0.9.0, because a 0.x project may break compatibility at any time. So when you read a dependency diff from the AI, check which digit moved: a patch (low risk), a minor (new behavior possible), or a major or 0.x minor (breaking changes possible).
Cargo.lock answers "which exact version is installed". On the first build, Cargo resolves each range to an exact version and writes it into the lockfile — version = "1" may end up pinned to one specific tokio release. Commit the lockfile (jackdaw's tree has it): you and any machine that rebuilds get identical dependencies, and as long as the manifest doesn't change, only cargo update re-resolves.
Features answer "which optional capabilities of a crate get compiled in". A feature you leave off takes its code out of the build — smaller dependency tree, faster compiles. You already saw the payoff of serde's derive in Chapter 6: the #[derive(Serialize, Deserialize)] macros exist because that feature is on. reqwest's stream is on so the gateway can read an SSE response body chunk by chunk (Chapter 12 unpacks that). tokio's "full" is a teaching-project shortcut; when a feature is missing, the compiler error usually points straight at what's missing — add it and move on.
Summary
Back to the opening questions: the tree sorts into five groups with distinct jobs, the entry points are main.rs and lib.rs, and artifacts live in a deletable target/. Cargo.toml splits into metadata, dependencies, and release settings. "1" is a range, the lockfile pins exact versions, features are compile-time switches. check, test, clippy, and fmt each answer one question. The next chapter leaves this project behind for crates.io: where these dependencies come from, and how to judge whether a crate is reliable.
Self-Check
- You come from Node.js: which of package.json / package-lock.json / node_modules do
Cargo.toml,Cargo.lock, andtarget/correspond to? Which of them belong in git, and why? - The AI "simplifies"
serde = { version = "1", features = ["derive"] }intoserde = "1". Predict whatcargo checkreports, and give your reasoning. - Why does
main.rssayuse jackdaw::cli::Cli, while files inside the library refer to each other withcrate::paths? What does that tell you about the relationship between the binary crate and the library crate?
Answers
- In order: package.json (what you declare), package-lock.json (exact versions pinned), node_modules (local artifacts). Git gets the first two plus your source:
target/is pure output — rebuildable any time, large, platform-specific. The lockfile goes into git for reproducible builds — the same practice as most ecosystems' lockfiles; Cargo.lock pins every transitive dependency too. - Compilation fails. The
#[derive(Serialize, Deserialize)]you used in Chapter 6 depends on serde'sderivefeature. Features are real compile-time switches; with it off, the derive macros don't exist, and every type definition using them errors out. - The binary crate and the library crate inside one package are two independent compilation units.
crate::means "the root of the current crate" — and formain.rs, that root is itself. To use anything from the library, it must go through the library's external name,jackdaw::. Somain.rsis not a "master entry file" — it's the library's first customer; the library is where all the reusable logic lives.