Rust Cargo & Crates
On the Get Started page you used cargo new and cargo run. That was only the beginning. Cargo is Rust’s build tool and its package manager. It compiles your code, runs your tests, formats your files, and downloads libraries other people have written.
Those libraries are called crates. Think of crates like apps in an app store: someone else has already solved a problem (reading JSON, making web requests, parsing dates), and you can add their solution to your project with one command. This page shows you how that works, and how to choose a good crate.
Crates and packages
Section titled “Crates and packages”Two words come up all the time:
- A crate is one unit of Rust code that the compiler builds. It is either a binary (a program you can run, starting at
src/main.rs) or a library (code for other programs to use, starting atsrc/lib.rs). - A package is what
cargo newcreates: a folder with aCargo.tomlfile and one or more crates inside.
In everyday talk, people say “crate” for both. When someone says “use the serde crate”, they mean “add the serde package from crates.io to your project”.
Inside Cargo.toml
Section titled “Inside Cargo.toml”Every package has a Cargo.toml file. It is written in TOML, a simple settings format. Here is what cargo new hello_rust gives you:
[package]name = "hello_rust"version = "0.1.0"edition = "2024"
[dependencies][package]describes your project.nameandversionare what other people would see if you published it.editionpicks which edition of the Rust language to use. Editions (2018, 2021, 2024) let Rust improve without breaking old code. Just keep whatevercargo newwrote.[dependencies]lists the crates your code uses. It starts empty.
As a project grows, you may see more sections:
[dependencies]serde = { version = "1.0", features = ["derive"] }serde_json = "1.0"
[dev-dependencies]# only used by tests and examples, not by your real programpretty_assertions = "1.4"
[profile.release]# settings for `cargo build --release`opt-level = 3Adding a dependency with cargo add
Section titled “Adding a dependency with cargo add”You can edit [dependencies] by hand, but the easiest way is cargo add. It finds the latest version on crates.io and writes the line for you:
cargo add serde_jsonSome crates have optional parts called features. They are switched off by default to keep your build small. You turn them on with --features:
cargo add serde --features deriveAfter these two commands, your Cargo.toml will contain something like this:
[dependencies]serde = { version = "1.0.228", features = ["derive"] }serde_json = "1.0.145"Now use the crate in your code. The next cargo run downloads it, compiles it once, and links it into your program:
use serde_json::json;
fn main() { let crab = json!({ "legs": 10, "name": "Ferris" });
println!("{}", crab); println!("Name: {}", crab["name"]);}Output:
{"legs":10,"name":"Ferris"}Name: "Ferris"The quotes around "Ferris" are there because crab["name"] is still a JSON value, not a plain Rust string.
The Rust Playground already includes the most popular crates, so this runs there without cargo add. On your own machine, you need the cargo add serde_json step first. You’ll learn much more about this crate on the JSON with Serde page.
To remove a crate you no longer need, run cargo remove serde_json.
Version requirements and Cargo.lock
Section titled “Version requirements and Cargo.lock”When you write serde_json = "1.0", you are not asking for exactly version 1.0. You are saying “any version that is compatible with 1.0”. Cargo follows semantic versioning (SemVer): a version is MAJOR.MINOR.PATCH, and only a change in the major number is allowed to break your code.
| You write | Cargo may use | Why |
|---|---|---|
"1.0" (same as "^1.0") |
>=1.0.0 and <2.0.0 |
1.x versions should all be compatible |
"1.4.2" |
>=1.4.2 and <2.0.0 |
at least 1.4.2, but still 1.x |
"0.4" |
>=0.4.0 and <0.5.0 |
before 1.0, the minor number counts as breaking |
"~1.4" |
>=1.4.0 and <1.5.0 |
only patch updates |
"=1.4.2" |
exactly 1.4.2 |
rarely needed |
So who decides the exact version? That is the job of Cargo.lock. The first time you build, Cargo picks the newest versions that match your requirements and writes them down in Cargo.lock. Every build after that uses the same exact versions, on your machine and on everyone else’s.
- Don’t edit
Cargo.lockby hand. Cargo manages it. - Commit it to git, especially for programs. Then your teammates and your CI build exactly what you built.
cargo updatemoves to the newest versions that still matchCargo.toml, and rewritesCargo.lock. It never jumps to a new major version; for that you changeCargo.tomlyourself.
Finding a good crate
Section titled “Finding a good crate”There are three websites you will use again and again:
- crates.io is the official registry. Every public crate lives here. Search for a word like “csv” or “date”.
- docs.rs hosts the documentation of every crate, built automatically. For example, docs.rs/serde_json.
- lib.rs is an unofficial, faster index that groups crates by category. It is great for browsing.
Many crates solve the same problem, so how do you choose? Before you add one, take a minute to check:
- Downloads. Popular crates have millions of downloads. That means many people found and fixed the bugs before you.
- Recent updates. Look at the date of the latest release. A crate that has not changed in years may still be fine (some are simply “done”), but check that its repository is not full of unanswered issues.
- Documentation. Open its docs.rs page. Good crates have a clear front page with an example you can copy.
- Version 1.0 or higher. Not required, but it tells you the author promises a stable API.
- Few dependencies. Every crate you add brings its own dependencies. Smaller is easier to trust.
When in doubt, search for “rust” plus your problem and see which crate the community recommends again and again.
Essential Cargo commands
Section titled “Essential Cargo commands”You don’t need to memorize these. Keep this table nearby and you’ll learn them by using them:
| Command | What it does |
|---|---|
cargo new my_app |
Create a new binary project (add --lib for a library) |
cargo build |
Compile into target/debug/ |
cargo run |
Compile and run (pass arguments after --: cargo run -- input.txt) |
cargo check |
Check for errors without building a program. Much faster than build |
cargo test |
Run your tests (see Testing) |
cargo fmt |
Format all your code in the standard style |
cargo clippy |
Run the linter. It suggests simpler, more idiomatic code |
cargo doc --open |
Build HTML docs for your project and every dependency, then open them |
cargo update |
Update Cargo.lock to the newest compatible versions |
cargo build --release |
Compile an optimized program into target/release/ |
A good habit: run cargo check often while you write, and cargo fmt and cargo clippy before you commit. cargo doc --open is also a hidden gem. It gives you the docs for the exact crate versions you use, and it works offline.
Debug vs release builds
Section titled “Debug vs release builds”By default, Cargo makes a debug build. It compiles quickly and keeps extra safety checks, such as panicking when an integer overflows. That is perfect while you are developing.
A release build (cargo build --release or cargo run --release) takes longer to compile but runs much faster, often 10 times or more. Use it when you ship your program or measure its speed.
Your code can even tell which kind of build it is:
fn main() { if cfg!(debug_assertions) { println!("Debug build: extra checks are on"); } else { println!("Release build: optimized for speed"); }}Output:
Debug build: extra checks are onIn the Playground you can switch from “Debug” to “Release” in the menu and run it again to see the other line.
Binary and library crates together
Section titled “Binary and library crates together”A package can hold a library and a binary at the same time. This is a very common layout. You keep your real logic in src/lib.rs, and src/main.rs stays small and just calls it:
greeter/├── Cargo.toml└── src/ ├── lib.rs # the library crate: reusable logic └── main.rs # the binary crate: the program you runpub fn greet(name: &str) -> String { format!("Hello, {name}!")}fn main() { // the library is named after the package println!("{}", greeter::greet("Ferris"));}Why split it like this? Code in lib.rs is easy to test, and other programs (or a second binary) can reuse it. The Playground only has one file, but you can get the same feel with a pub mod, like on the Modules page:
mod greeter { pub fn greet(name: &str) -> String { format!("Hello, {name}!") }}
fn main() { println!("{}", greeter::greet("Ferris"));}Output:
Hello, Ferris!A quick look at workspaces
Section titled “A quick look at workspaces”When a project grows into several packages that belong together (for example, a command-line app, a web server, and a shared library), you can put them in one workspace. They share one Cargo.lock and one target/ folder, so everything builds together and uses the same dependency versions.
The top-level Cargo.toml lists the members:
[workspace]members = ["app", "core"]resolver = "3"my_project/├── Cargo.toml # the workspace file above├── Cargo.lock # shared by all members├── app/ # a binary package└── core/ # a library packageThen cargo build at the top builds everything, and cargo run -p app runs one member. You won’t need this for your first projects, but it is good to recognize it when you read other people’s code.
Common mistakes
Section titled “Common mistakes”Using a crate without adding it. If you write use serde_json::json; but forgot cargo add serde_json, the compiler can’t find it:
error[E0432]: unresolved import `serde_json` --> src/main.rs:1:5 |1 | use serde_json::json; | ^^^^^^^^^^ use of unresolved module or unlinked crate `serde_json` | = help: you might be missing a crate named `serde_json`The fix is to run cargo add serde_json. Also check the spelling: crate names use - or _, and in code a - always becomes _.
Forgetting a feature. If #[derive(Serialize)] says it cannot find the derive macro, you probably added serde without --features derive. Run cargo add serde --features derive again; it updates the existing line.
Measuring speed with a debug build. If your program feels slow, try cargo run --release before you change any code.
Practice
Section titled “Practice”1. Use a crate from the Playground
Section titled “1. Use a crate from the Playground”Use the json! macro from serde_json to describe a crate with three keys: downloads (a number), name and version. Print the whole value, then print just the version.
Hint: copy the shape of the crab example in Adding a dependency with cargo add. On your own machine, run cargo add serde_json first; the Playground already has it.
Show solution
use serde_json::json;
fn main() { let krate = json!({ "downloads": 500, "name": "serde_json", "version": "1.0" });
println!("{}", krate); println!("Version: {}", krate["version"]);}Output:
{"downloads":500,"name":"serde_json","version":"1.0"}Version: "1.0"The variable is called krate because crate is a reserved word in Rust. That spelling is a common joke in Rust code. As before, the version prints with quotes because it’s still a JSON value; the JSON with Serde page shows how to get plain text out.
2. Messages only in debug builds
Section titled “2. Messages only in debug builds”Write a function debug_log(msg: &str) that prints [debug] followed by the message, but only in a debug build. Use it in main around a small calculation, and print the result normally.
Hint: wrap the println! in if cfg!(debug_assertions) { ... }, like in Debug vs release builds.
Show solution
fn debug_log(msg: &str) { if cfg!(debug_assertions) { println!("[debug] {msg}"); }}
fn main() { debug_log("starting the calculation"); let mut total = 0; for n in 1..=10 { total += n; } debug_log("calculation done");
println!("The sum of 1 to 10 is {total}");}Output:
[debug] starting the calculation[debug] calculation doneThe sum of 1 to 10 is 55Switch the Playground to “Release” (or use cargo run --release locally) and run it again: only the last line prints. For real projects, the Logging page shows crates that do this properly, with levels like debug and info.
- Cargo builds, runs, tests, and formats your code, and it downloads crates from crates.io.
Cargo.tomlis what you ask for;Cargo.lockrecords the exact versions you got. Commit both.cargo add name --features xadds a dependency."1.0"means “any compatible 1.x version”.- Judge a crate by downloads, recent updates, and good docs on docs.rs.
- Use
cargo checkwhile writing,cargo fmtandcargo clippybefore committing, and--releasefor speed. - Put reusable logic in
src/lib.rsand keepsrc/main.rssmall.
Quick check
Section titled “Quick check”Quick check
1. In Cargo.toml, what does serde_json = "1.0" mean?
2. Which file records the exact versions of every dependency your build used?
3. You want to check your code for errors as fast as possible, without producing a program. Which command fits best?
4. How do you add serde with its derive feature turned on?
Score: 0 / 4
Read more
Section titled “Read more”- The Cargo Book: the official guide and reference for everything Cargo does.
- Specifying Dependencies — The Cargo Book: every way to write a version requirement, plus git and path dependencies.
cargo add— The Cargo Book: all the options ofcargo add, like--dev,--featuresand--no-default-features.- More about Cargo and Crates.io — The Rust Book: release profiles, publishing your own crate, and installing tools with
cargo install. - Cargo Workspaces — The Rust Book: a step-by-step workspace with a binary and two libraries.