Adding dependencies to Rust projects is surprisingly flexible, and cargo add isn’t just for adding new crates; it’s also your primary tool for managing existing dependencies, letting you change versions and features without manually editing Cargo.toml.

Let’s see it in action. Imagine you have a project, and you need to add the reqwest crate for HTTP requests.

cargo add reqwest

This command does more than just slap reqwest = "0.11.18" into your Cargo.toml. It fetches the latest compatible version, checks for transitive dependencies, and updates your Cargo.lock file to ensure reproducible builds.

Here’s what your Cargo.toml might look like after running cargo add reqwest:

[package]
name = "my_project"
version = "0.1.0"
edition = "2021"

[dependencies]
reqwest = "0.11.18"

But what if you need a specific feature, like the json feature for reqwest?

cargo add reqwest --features json

This would update your Cargo.toml to:

[package]
name = "my_project"
version = "0.1.0"
edition = "2021"

[dependencies]
reqwest = { version = "0.11.18", features = ["json"] }

This cargo add command is the modern, recommended way to manage dependencies. It integrates directly with Cargo.toml and Cargo.lock, ensuring consistency and preventing common errors. It’s not just about adding; it’s about maintaining a healthy dependency graph.

The mental model here is that cargo add is your single source of truth for dependency declarations. It abstracts away the manual editing of Cargo.toml, which is prone to syntax errors and versioning mistakes. When you run cargo add, Cargo performs several crucial steps:

  1. Parses Cargo.toml: It reads your existing Cargo.toml file.
  2. Resolves Dependency: It consults the crates.io registry to find the specified crate and the requested version (or the latest compatible version if none is specified). It also resolves all transitive dependencies required by your new dependency.
  3. Updates Cargo.toml: It writes the new dependency information into the [dependencies] section of Cargo.toml using the correct TOML syntax, including any specified features or optional dependencies.
  4. Updates Cargo.lock: It generates or updates the Cargo.lock file. This file records the exact versions of all dependencies (direct and transitive) that were resolved, ensuring that future builds use the exact same versions, making your builds reproducible.

Consider a scenario where you need to upgrade a dependency. You don’t edit Cargo.toml by hand. Instead, you might specify a new version:

cargo add tokio@1.28.2

This command will find the tokio entry in your Cargo.toml and update it to tokio = "1.28.2". If tokio was already present, it updates the version. If not, it adds it. Crucially, it then re-resolves the entire dependency graph with the new tokio version and updates Cargo.lock.

You can also remove dependencies with cargo remove:

cargo remove serde

This removes serde from Cargo.toml and updates Cargo.lock. It’s the symmetrical operation to cargo add.

The system handles different dependency types too. For example, adding a build dependency:

cargo add quote --build

This adds quote to the [build-dependencies] section in Cargo.toml.

cargo add handles semantic versioning rules automatically. If you add rand, it will default to a compatible version like rand = "0.8.5". If you later run cargo add rand@0.7.0, it will downgrade rand to that specific version and re-resolve. If you run cargo add rand@">=0.8.0,<0.9.0", it will update the Cargo.toml to reflect that version requirement.

The real power is in how cargo add orchestrates the entire dependency resolution and locking process. It ensures that when you add or change a dependency, the entire ecosystem of your project’s dependencies remains consistent. Without cargo add, you’d be manually editing Cargo.toml, then running cargo build or cargo check hoping that Cargo could resolve the new dependencies without conflicts, and then manually trying to update Cargo.lock if needed. cargo add automates this complex dance.

When you specify a crate without a version, like cargo add anyhow, Cargo will fetch the latest version from crates.io that satisfies the default versioning policy (usually the latest stable release compatible with your current Rust toolchain). This is convenient but can lead to unexpected changes if a new major version is released. For critical dependencies or libraries, it’s often better to pin to a specific version or a narrow range using cargo add crate_name@version or cargo add crate_name="^x.y.z". The ^ symbol, also known as the caret requirement, allows updates to the patch and minor versions but not the major version. For example, rand = "^0.8.5" allows 0.8.6 and 0.8.7 but would prevent an update to 0.9.0.

The next step after managing your dependencies is to understand how to conditionally compile code based on these dependencies using features.

Want structured learning?

Take the full Cargo course →