13.5Binary and library crates
You've been saying "crate" since lesson 7.9, when cargo add rand pulled one in. This lesson pins the word down and draws a distinction that shapes how real Rust projects are laid out: a crate comes in two kinds, binary and library, and a single package often has both. Getting this split right is what makes your code testable and reusable, which is why the testing chapter (14) depends on it.
Crate, package, the vocabulary
A crate is the unit the Rust compiler compiles at once: a tree of modules (lessons 13.1 through 13.4) with a single root file. A package is what cargo new creates: a Cargo.toml plus one or more crates. Most packages you've made so far were one binary crate, but the package is the thing you publish and the crate is the thing that compiles.
There are two kinds of crate:
A binary crate compiles to an executable you can run. Its root is src/main.rs, and it must have a fn main (lesson 1.1). Every program in the course so far has been a binary crate; cargo run builds and runs it.
A library crate has no main and compiles to nothing you run directly. Instead it provides code for other crates to use. Its root is src/lib.rs. The rand crate you added is a library crate: you don't run rand, you call into it.
One package, both crates
Here's the layout that matters. A single package can contain both a library crate and a binary crate: src/lib.rs holds the real logic, and src/main.rs is a thin wrapper that calls into the library.
my_tool/
├── Cargo.toml
└── src/
├── lib.rs ← the library crate: all the logic
└── main.rs ← the binary crate: a thin shell calling the library// src/lib.rs
pub fn run(input: &str) -> usize {
input.split_whitespace().count()
}// src/main.rs
use my_tool::run;
fn main() {
let count = run("the quick brown fox");
println!("{count} words");
}4 words
Notice use my_tool::run; in main.rs. The binary refers to the library by the package's name (my_tool, set in Cargo.toml), exactly as it would refer to any external dependency. From the binary's point of view, your own library is just another crate it depends on, reached through its public API. Anything the binary needs from the library must therefore be pub (lesson 13.3).
Key insight
Put the logic in lib.rs and keep main.rs thin. The binary should do little more than gather inputs (command-line arguments, files), hand them to library functions, and report results. Everything worth testing lives in the library, where tests can reach it through the public API. A fat main.rs full of logic is hard to test, because main itself isn't easily callable; a thin main.rs over a real library is the standard shape of a well-built Rust tool.
Why the split matters for testing
This is the IOU to chapter 14. Rust's integration tests (lesson 14.4) live in a tests/ directory and can only see a crate's public API, the same way an outside user would. They can test a library crate, because a library has a public API. They can't meaningfully test a binary crate, because a binary exposes only an executable, not callable functions.
So the reason to split logic into lib.rs isn't bureaucratic. It's that the library is the part you can test thoroughly: unit tests inside it (lesson 14.3) and integration tests against its public surface (lesson 14.4). The binary becomes a shell so small there's almost nothing left in it to break. When chapter 20 builds a real command-line tool, this lib.rs-plus-thin-main.rs structure is exactly the shape it uses, and the testability is the payoff.
Multiple binaries
One more layout you'll meet: a package can ship several binaries by putting extra ones in src/bin/. Each file there (src/bin/tool_a.rs, src/bin/tool_b.rs) becomes its own binary crate, runnable with cargo run --bin tool_a. This is handy for a project that offers a few related command-line tools sharing one library. You won't need it often, but it explains why cargo run sometimes asks which binary you mean.
Quiz time
Question #1
What's the difference between a binary crate and a library crate, and what is each one's root file?
Show solution
A binary crate compiles to an executable and must have a fn main; its root is src/main.rs. A library crate has no main, compiles to code for other crates to use, and its root is src/lib.rs. A single package can contain both.
Question #2
Why is it good practice to put a program's logic in lib.rs and keep main.rs thin?
Show solution
Because the library crate has a public API that tests (especially integration tests in tests/) can call, while a binary exposes only an executable that's hard to test. Putting logic in the library makes it thoroughly testable; keeping main.rs to input-gathering and reporting leaves almost nothing untestable in the binary. It also makes the logic reusable by other crates.
Question #3
In the two-crate example, why must run be marked pub?
Show solution
Because main.rs is a separate crate that depends on the library crate, and it reaches run through the library's public API (use my_tool::run;). Items are private by default (lesson 13.3), so a private run would be invisible to the binary, just as it would be to any external user of the library.
You now understand crates as compile units and the library/binary split. The next lesson surveys the tool that orchestrates it all, Cargo: build profiles, cargo doc, a glance at workspaces and features.