Coding by Hand
Rust home

What Is a Crate

A crate is one baseball card. A package is the binder that holds the card and tells the world whose binder it is. The card shop downtown that prints and trades these cards is crates.io. Every Rust project you ever write is some shape of this — one binder, one or two cards, maybe a stack of cards you traded in from the shop. Once you see the binder and the cards as two different objects, the rest of Cargo stops feeling like magic.

A crate is a binder: src/ holds the code, Cargo.toml is the table of contents.
A crate is a binder: src/ holds the code, Cargo.toml is the table of contents.

A crate is the smallest thing the Rust compiler will compile in one go. You hand it one card, it stamps out one piece of machine code. That is the whole definition. A card comes in one of two flavors. A binary card has a main function on it — when the compiler stamps a binary card, the output is a program you can run by typing its name in the terminal. A library card has no main. It has functions and types that other cards can pull off the shelf and use. The compiler stamps a library card into a .rlib file, a chunk of compiled code waiting to be linked into someone else's binary. One card, one flavor. Mixing the two on the same card is not a thing in Rust.

A package is the binder. The binder has a label on the front called Cargo.toml, and that label is the one piece of paper that tells Cargo everything it needs to know about what is inside. The name of the package. The version. Which cards are in the binder and where their source files live. Which cards from the shop you want pulled and slipped into the binder before the next print run. A binder always has exactly one Cargo.toml. If you see two Cargo.toml files, you are looking at two packages, not one.

fn show_manifest() {
    let toml = [
        "[package]",
        "name = \"hello_crate\"",
        "version = \"0.1.0\"",
        "edition = \"2024\"",
        "",
        "[dependencies]",
        "serde = \"1.0.219\"",
        "",
        "[[bin]]",
        "name = \"hello_crate\"",
        "path = \"src/main.rs\"",
    ];
    println!("-- Cargo.toml: the order ticket for one package --");
    for line in toml {
        println!("{line}");
    }
    println!();
}

A binder can hold at most one library card, but it can hold any number of binary cards. The library card lives at src/lib.rs if it is in the binder at all. Binary cards live at src/main.rs for the default binary, or at src/bin/<name>.rs for any extras. The two card types are siblings in the same binder — they are not parent and child. A binary card in the same binder calls into the library card the same way any outside binder would, by naming it. That symmetry is intentional. It means the binary you ship and the library you publish to crates.io are testing the same code.

One package can contain multiple crates: at most one library and any number of binaries.
One package can contain multiple crates: at most one library and any number of binaries.

The card shop is crates.io, and it opened the same day Rust 1.0 did, in May 2015. Yehuda Katz and Carl Lerche had finished Cargo the year before, and the Rust team decided from the start that there would be one official place to publish and download cards — no fighting over which shop to trust the way C and C++ had. C never shipped with a package manager. To this day a serious C project pulls libraries from whatever your operating system's package list calls them, or from a tarball someone hosts on a personal server, or from a git submodule you have to remember to update. Every C team invents its own protocol for this and every C team gets it slightly wrong. The Rust team had watched npm and Ruby's Bundler eat the dependency problem for JavaScript and Ruby — Bundler had shipped in 2010 and was the model — and they wanted that, but with a single first-party registry instead of a free-for-all. Cargo and crates.io shipped together in 2014, and the first crate published was libc on November 10 of that year.

crates.io is the Rust package registry: a public catalog of every published crate.
crates.io is the Rust package registry: a public catalog of every published crate.

The card shop prints a version number on every card it sells, and the number reads MAJOR.MINOR.PATCH. The rules are called semver, short for semantic versioning, and they are the same rules npm and Bundler use. Bump PATCH for a bug fix that breaks nothing. Bump MINOR for a new feature that breaks nothing existing. Bump MAJOR when you change something that will break code that already uses the card. The number is the card's promise to you. When you write serde = "1.0.219" in your Cargo.toml, you are telling Cargo to find a card whose MAJOR is exactly 1, whose MINOR is at least 0, and whose PATCH is at least 219. Cargo will happily accept 1.4.0 because the MAJOR matches. It will refuse 2.0.0 because that is the maintainer's way of saying "I broke things."

Cargo records the exact version it actually picked in a second file called Cargo.lock. The label Cargo.toml says what version range is allowed; the lockfile says what version got installed. Commit the lockfile to git. The next person who clones your binder and runs cargo build gets exactly the cards you got, not a newer one the shop printed yesterday. This is the same reason npm has package-lock.json and pip has requirements.txt with pinned versions. The lockfile is the bottom of the bag — what is actually inside.

You can edit Cargo.toml by hand to add a card. Open it in your editor, type serde = "1.0" under [dependencies], save, and the next cargo build will pull it from the shop. Or you can let Cargo edit the file for you with one command, which is the way most Rust programmers do it because it picks the current latest version off the shop's shelf without you having to look it up.

cargo add serde
cargo add tokio --features rt-multi-thread,macros

cargo add reads the current latest version from crates.io, writes the right line into [dependencies], and turns on any features you asked for. The next cargo build downloads the card, compiles it once, caches it, and links it into your binder. Subsequent builds reuse the cached card and skip the download. The shop is on the internet, but your build is on disk.

Now put the cards together. The crate below is what the rest of this lesson is about — a tiny library card sitting inside the same binder as a binary card, and the binary calling the library by name. The library card here is the mod my_lib { ... } block. In a real binder it would be a separate file at src/lib.rs, but for this lesson it lives inline in main.rs so you can see both cards on one page.

mod my_lib {
    pub fn greet(name: &str) -> String {
        format!("hello, {name}")
    }

    pub fn add(a: i32, b: i32) -> i32 {
        a + b
    }
}

The library card has two public functions, greet and add. The word pub means "anyone holding this card can call this function." Without pub, the function exists but cannot be reached from outside the card. That visibility rule is the seam between the library card and every binder that ever uses it — Rust will not let you accidentally depend on something the library author did not promise to keep stable.

fn main() {
    show_manifest();
    show_package_tree();
    show_calls();
    show_registry();
}

fn show_calls() {
    println!("-- the binary crate calling the library crate --");
    println!("my_lib::greet(\"aarit\") -> {}", my_lib::greet("aarit"));
    println!("my_lib::add(2, 3)        -> {}", my_lib::add(2, 3));
    println!();
}

The binary card has fn main on it, the way every binary card does. Inside main it calls my_lib::greet("aarit") and my_lib::add(2, 3). The :: is how Rust spells "the thing named greet, inside the card named my_lib." Run the whole package with cargo run and the binary card executes, calling into the library card the way any other binder on crates.io would. Here is the full program's output, with the manifest, the tree, the calls, and the semver rules in order.

-- Cargo.toml: the order ticket for one package --
[package]
name = "hello_crate"
version = "0.1.0"
edition = "2024"

[dependencies]
serde = "1.0.219"

[[bin]]
name = "hello_crate"
path = "src/main.rs"

-- one package, two crates side by side --
hello_crate/
  Cargo.toml      <- one manifest per package
  Cargo.lock      <- exact versions cargo resolved
  src/
    main.rs       <- binary crate (entry: fn main)
    lib.rs        <- library crate (entry: pub items)
  target/         <- build output (gitignored)

-- the binary crate calling the library crate --
my_lib::greet("aarit") -> hello, aarit
my_lib::add(2, 3)        -> 5

-- crates.io versions, read as MAJOR.MINOR.PATCH --
1.0.219    MAJOR=1  MINOR=0  PATCH=219
^1.0       any 1.x.y where x >= 0 (no 2.0.0)
=1.0.219   exactly 1.0.219, nothing else
1.2.*      any 1.2.y patch release

cargo add serde            <- edits Cargo.toml for you
cargo build                <- fetches from crates.io, locks the version

Read the output top to bottom. The first block is the Cargo.toml for an imaginary hello_crate package — every line in it is mandatory in some shape, and every line means one thing. The second block is the layout of the binder on disk. Notice that src/main.rs and src/lib.rs both live in the same src/ folder; Cargo's convention picks them out by filename, not by directory. The third block is the binary calling the library. The fourth block is four ways to write a version requirement, ordered loose to strict — 1.0.219 is the loose default, =1.0.219 is the strict pin.

A question to keep in your head. If two binders on crates.io both depend on serde = "1.0", and Cargo resolves them both to serde 1.0.219, how many copies of serde does the final binary contain? One. Cargo deduplicates dependencies as long as their MAJOR versions match. If one binder pinned serde = "1.0" and the other pinned serde = "2.0", you would end up with both copies linked in, and the two halves of your program could not exchange a serde type because Rust treats them as different types — same name, different version, different cards. That is why MAJOR is the version number that matters.

Semantic versioning splits one number into three parts so callers know what kinds of changes to expect.
Semantic versioning splits one number into three parts so callers know what kinds of changes to expect.

The next bottleneck is that one binder of cards is only useful if every other Rust programmer can find the cards inside it — which is the next lesson, where the binder gets published to crates.io and the rendered docs at docs.rs become the first thing anyone reads about it.