Skip to content

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.

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 at src/lib.rs).
  • A package is what cargo new creates: a folder with a Cargo.toml file 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”.

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. name and version are what other people would see if you published it.
  • edition picks which edition of the Rust language to use. Editions (2018, 2021, 2024) let Rust improve without breaking old code. Just keep whatever cargo new wrote.
  • [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 program
pretty_assertions = "1.4"
[profile.release]
# settings for `cargo build --release`
opt-level = 3

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:

Terminal window
cargo add serde_json

Some crates have optional parts called features. They are switched off by default to keep your build small. You turn them on with --features:

Terminal window
cargo add serde --features derive

After 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"
▶ Try it Yourself

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.

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.lock by hand. Cargo manages it.
  • Commit it to git, especially for programs. Then your teammates and your CI build exactly what you built.
  • cargo update moves to the newest versions that still match Cargo.toml, and rewrites Cargo.lock. It never jumps to a new major version; for that you change Cargo.toml yourself.

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:

  1. Downloads. Popular crates have millions of downloads. That means many people found and fixed the bugs before you.
  2. 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.
  3. Documentation. Open its docs.rs page. Good crates have a clear front page with an example you can copy.
  4. Version 1.0 or higher. Not required, but it tells you the author promises a stable API.
  5. 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.

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.

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 on
▶ Try it Yourself

In the Playground you can switch from “Debug” to “Release” in the menu and run it again to see the other line.

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 run
src/lib.rs
pub fn greet(name: &str) -> String {
format!("Hello, {name}!")
}
src/main.rs
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!
▶ Try it Yourself

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 package

Then 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.

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.

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"
▶ Try it Yourself

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.

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 done
The sum of 1 to 10 is 55
▶ Try it Yourself

Switch 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.toml is what you ask for; Cargo.lock records the exact versions you got. Commit both.
  • cargo add name --features x adds 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 check while writing, cargo fmt and cargo clippy before committing, and --release for speed.
  • Put reusable logic in src/lib.rs and keep src/main.rs small.

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