Understanding Cargo
13 min read
Cargo is Rust’s build system and package manager. Think of it as npm, webpack, jest, prettier, and eslint all combined into one tool.
Quick Overview
Section titled “Quick Overview”Cargo is the Rust tool. It handles:
- Project creation
- Dependency management
- Building and compiling
- Running tests
- Generating documentation
- Publishing packages
- Running benchmarks
- And much more!
Time required: 30-40 minutes
TypeScript/JavaScript Comparison
Section titled “TypeScript/JavaScript Comparison”Node.js ecosystem (multiple tools):
# Package managernpm initnpm install expressnpm install -D typescript
# Build toolnpx webpack
# Test runnernpx jest
# Formatternpx prettier --write .
# Linternpx eslint .
# Documentationnpx typedocRust (one tool - Cargo):
# All-in-one toolcargo new my_project # Create projectcargo add axum # Add dependencycargo build # Buildcargo test # Run testscargo fmt # Format codecargo clippy # Lintcargo doc # Generate docsEverything through one consistent interface!
Rust Equivalent: Creating a Project
Section titled “Rust Equivalent: Creating a Project”TypeScript/Node.js:
# Create directorymkdir my-projectcd my-project
# Initializenpm init -y
# Install TypeScriptnpm install -D typescript @types/node
# Create tsconfig.jsonnpx tsc --init
# Create src directorymkdir srcecho 'console.log("Hello");' > src/index.ts
# Add scripts to package.json (manual)# ...
# Runnpx ts-node src/index.tsResult: package.json, tsconfig.json, node_modules/, src/
Rust:
# Create project (one command!)cargo new my-projectcd my-project
# Runcargo runResult: Cargo.toml, src/main.rs, ready to go!
That’s it! No configuration needed, batteries included.
Detailed Explanation
Section titled “Detailed Explanation”Project Structure
Section titled “Project Structure”When you run cargo new my_project, you get:
my_project/├── Cargo.toml # Project manifest (like package.json)├── .git/ # Git repository (auto-initialized)├── .gitignore # Ignores /target (just one line)└── src/ └── main.rs # Entry point (with Hello World)Compare to Node.js/TypeScript:
my-project/├── package.json # Project manifest├── tsconfig.json # TypeScript config├── .gitignore # Manual creation├── node_modules/ # Dependencies (can be huge!)└── src/ └── index.ts # Entry point (empty)Cargo.toml vs package.json
Section titled “Cargo.toml vs package.json”Cargo.toml (Rust):
[package]name = "my_project"version = "0.1.0"edition = "2024"
[dependencies]# Dependencies go hereNote: You don’t pick the
editionyourself.cargo newwrites the newest edition supported by your toolchain intoCargo.tomlautomatically — on a recent stable toolchain that is"2024"(the latest stable edition). Editions are opt-in language revisions; existing code keeps compiling because crates of different editions interoperate.
package.json (Node.js):
{ "name": "my-project", "version": "0.1.0", "main": "dist/index.js", "scripts": { "build": "tsc", "start": "node dist/index.js", "dev": "ts-node src/index.ts", "test": "jest" }, "dependencies": {}, "devDependencies": { "typescript": "^5.0.0", "@types/node": "^18.0.0" }}Key differences:
- Cargo.toml is simpler (TOML vs JSON)
- No “scripts” section needed (cargo commands are standard)
- No “main” field (uses
src/main.rsorsrc/lib.rsby convention) - No devDependencies distinction (just
[dependencies]and[dev-dependencies])
Essential Cargo Commands
Section titled “Essential Cargo Commands”1. Project Creation
Section titled “1. Project Creation”# Create binary (executable) projectcargo new my_app
# Create library projectcargo new my_lib --libCompare to npm:
npm init -y # Creates package.json# But doesn't create any code structure2. Building
Section titled “2. Building”# Debug build (fast compile, slow runtime)cargo build
# Release build (slow compile, fast runtime)cargo build --release
# Check without building (faster)cargo checkOutput locations:
- Debug:
target/debug/my_app - Release:
target/release/my_app
Compare to TypeScript:
tsc # Compile to JavaScriptnpx webpack # Bundlenpm run build # (custom script)Build times comparison:
cargo build # 0.5s (debug)cargo build --release # 3-10s (release, but 10x faster execution)tsc # 0.1-1s (depends on project size)3. Running
Section titled “3. Running”# Build and run (debug)cargo run
# Build and run (release)cargo run --release
# Run with argumentscargo run -- arg1 arg2The -- separator passes arguments to your program, not to cargo.
# These arguments go to cargocargo run --release
# These arguments go to your programcargo run -- --helpCompare to Node.js:
node dist/index.js # Run compiled JSnpx ts-node src/index.ts # Run TS directlynpm start # (custom script)4. Testing
Section titled “4. Testing”# Run all testscargo test
# Run specific testcargo test test_name
# Run tests with outputcargo test -- --nocaptureCompare to JavaScript:
npm test # (jest, vitest, etc.)npm run test:watchCargo’s test runner is built-in, no configuration needed!
5. Dependencies
Section titled “5. Dependencies”# Add dependencycargo add serde
# Add dev dependencycargo add --dev criterion
# Add with specific versioncargo add serde --version 1.0
# Remove dependencycargo remove serde
# Update dependenciescargo updateCompare to npm:
npm install expressnpm install -D jestnpm install express@4.18.0npm uninstall expressnpm updateNote:
cargo addis built into Cargo (since Cargo 1.62, June 2022) — no extra install needed. You may still see older tutorials say “runcargo install cargo-editfirst”; that hasn’t been required for years.
You can always edit Cargo.toml by hand instead if you prefer:
[dependencies]serde = "1.0"6. Code Quality
Section titled “6. Code Quality”# Format code (like Prettier)cargo fmt
# Check formatting without changingcargo fmt -- --check
# Lint (like ESLint)cargo clippy
# Lint with auto-fix suggestionscargo clippy --fixCompare to JavaScript:
npx prettier --write .npx prettier --check .npx eslint .npx eslint --fix .7. Documentation
Section titled “7. Documentation”# Build documentationcargo doc
# Build and open in browsercargo doc --open
# Include private itemscargo doc --document-private-itemsCompare to TypeScript:
npx typedoc --out docs src/Cargo docs are generated from code comments:
/// Returns the sum of two numbers////// # Examples////// ```/// let result = add(2, 3);/// assert_eq!(result, 5);/// ```pub fn add(a: i32, b: i32) -> i32 { a + b}cargo doc automatically generates beautiful HTML documentation!
Key Differences from npm
Section titled “Key Differences from npm”1. No node_modules!
Section titled “1. No node_modules!”Node.js:
npm install# Creates node_modules/ (can be gigabytes!)# Need to .gitignore it# Re-download for every projectRust:
cargo build# Downloads to ~/.cargo/registry/ (shared across projects)# Only compiled artifacts in target/# Reused across projectsWhy it matters:
- Faster builds (shared cache)
- Less disk space
- Faster git operations (no node_modules to scan)
2. Lock Files
Section titled “2. Lock Files”package-lock.json (npm):
{ "name": "my-project", "lockfileVersion": 3, "requires": true, "packages": { "": { // ... hundreds of lines ... } }}- Always commit to git
- Ensures reproducible builds
- Can be huge (thousands of lines)
Cargo.lock (Rust):
# This file is automatically @generated by Cargo.# It is not intended for manual editing.[[package]]name = "my_project"version = "0.1.0"
[[package]]name = "serde"version = "1.0.152"- Commit for binaries, don’t commit for libraries
- Ensures reproducible builds
- Usually smaller
3. Scripts
Section titled “3. Scripts”package.json:
{ "scripts": { "build": "tsc", "start": "node dist/index.js", "dev": "ts-node src/index.ts", "test": "jest", "lint": "eslint .", "format": "prettier --write ." }}Cargo: No scripts needed! Everything is a standard command:
cargo build # Standardcargo run # Standardcargo test # Standardcargo fmt # Standardcargo clippy # StandardFor custom tasks (the equivalent of npm scripts), Cargo has a couple of native options and several community tools:
-
Cargo aliases — define command shortcuts in
.cargo/config.toml:.cargo/config.toml [alias]br = "build --release"Then
cargo brrunscargo build --release. Aliases only chain Cargo subcommands, not arbitrary shell. -
cargo xtask— a convention where build automation lives in an ordinary Rust binary you run withcargo xtask <task>. -
cargo-make— a third-party task runner (cargo install cargo-make) for richer, cross-platform task definitions.
Note: Unlike npm, Cargo has no built-in
scriptstable that runs shell commands. But most projects don’t need one — the standard subcommands cover the common workflow.
4. Semantic Versioning
Section titled “4. Semantic Versioning”Both use semver, but Cargo enforces it more strictly:
Cargo.toml:
[dependencies]serde = "1.0" # Caret range: >=1.0.0, <2.0.0tokio = "1.25.0" # ALSO a caret range: >=1.25.0, <2.0.0 (NOT exact!)tokio_exact = "=1.25.0" # THIS is exact: only 1.25.0regex = "1" # Caret range: >=1.0.0, <2.0.0Warning: A bare version string like
"1.25.0"is not an exact pin — Cargo treats it as the caret requirement>=1.25.0, <2.0.0. To require an exact version, prefix it with=, as in"=1.25.0".
package.json:
{ "dependencies": { "express": "^4.18.0", // 4.18.0 <= version < 5.0.0 "lodash": "4.17.21", // Exact "react": "^18" // Latest 18.x }}Cargo is more conservative with updates by default.
Common Pitfalls
Section titled “Common Pitfalls”Pitfall 1: Forgetting to Rebuild
Section titled “Pitfall 1: Forgetting to Rebuild”Problem:
# Edit main.rs# Run old binary./target/debug/my_app # Runs OLD version!Solution:
cargo run # Always rebuilds if neededOr:
cargo build./target/debug/my_app # Now runs new versionPitfall 2: Large target/ Directory
Section titled “Pitfall 2: Large target/ Directory”Problem:
du -sh target/# 5.0G target/ # Huge!Solution:
# Clean build artifactscargo clean
# Or clean all Rust projectscargo install cargo-cachecargo cache --autocleanWhy it gets big:
- Multiple debug/release builds
- Dependencies compiled for your project
- Test binaries
Compare to node_modules:
du -sh node_modules/# 500M node_modules/ # Also huge!But target/ can be cleaned and regenerated quickly.
Pitfall 3: Slow First Build
Section titled “Pitfall 3: Slow First Build”Problem:
cargo build# Compiling 150 crates... (takes 5 minutes)Why: Cargo compiles all dependencies from source.
Solutions:
- Use
cargo checkduring development:
cargo check # Faster, just type-checks- Use release mode only when needed:
cargo build # Debug mode (fast compile)# Only use --release for production- Use
sccachefor caching:
cargo install sccacheexport RUSTC_WRAPPER=sccachePitfall 4: Committing Cargo.lock (or not)
Section titled “Pitfall 4: Committing Cargo.lock (or not)”Rule:
- Binary projects: Commit
Cargo.lock - Library projects: Don’t commit
Cargo.lock
Why:
- Binaries need reproducible builds
- Libraries want to test with latest compatible versions
Add to .gitignore (for libraries):
/target//Cargo.lockBest Practices
Section titled “Best Practices”1. Use cargo fmt on Save
Section titled “1. Use cargo fmt on Save”VS Code settings.json:
{ "[rust]": { "editor.formatOnSave": true }}Or manually:
cargo fmtBefore commit:
cargo fmt --check # Verify formatting2. Run clippy Before Commit
Section titled “2. Run clippy Before Commit”cargo clippyOr make it strict:
cargo clippy -- -D warnings # Treat warnings as errorsCI integration:
- name: Run clippy run: cargo clippy -- -D warnings3. Use cargo check During Development
Section titled “3. Use cargo check During Development”Instead of:
cargo build # Slow: creates executableUse:
cargo check # Fast: only type-checks3-5x faster! Use during development, cargo build only when you need to run.
4. Separate Dev and Prod Dependencies
Section titled “4. Separate Dev and Prod Dependencies”[dependencies]# Production dependenciesserde = "1.0"tokio = "1.25"
[dev-dependencies]# Only used in tests/benchmarkscriterion = "0.5"mock_instant = "0.3"Compare to package.json:
{ "dependencies": { /* prod */ }, "devDependencies": { /* dev only */ }}5. Use Workspaces for Monorepos
Section titled “5. Use Workspaces for Monorepos”my-workspace/├── Cargo.toml # Workspace manifest├── api/│ ├── Cargo.toml│ └── src/├── cli/│ ├── Cargo.toml│ └── src/└── shared/ ├── Cargo.toml └── src/Root Cargo.toml:
[workspace]members = ["api", "cli", "shared"]Compare to npm workspaces:
{ "workspaces": ["packages/*"]}Similar concept, different implementation.
Real-World Example: Web API Project
Section titled “Real-World Example: Web API Project”Create project:
cargo new web_apicd web_apiAdd dependencies:
cargo add axumcargo add tokio --features fullcargo add serde --features deriveOr manually edit Cargo.toml:
[package]name = "web_api"version = "0.1.0"edition = "2024"
[dependencies]axum = "0.8"tokio = { version = "1", features = ["full"] }serde = { version = "1.0", features = ["derive"] }
[profile.release]opt-level = 3 # Maximum optimizationlto = true # Link-time optimizationcodegen-units = 1 # Better optimization, slower compileBuild and run:
# Developmentcargo run
# Productioncargo build --release./target/release/web_apiCompare to Node.js/Express:
npm init -ynpm install express typescript @types/node @types/expressnpm install -D ts-node nodemon# Edit tsconfig.json# Edit package.json scriptsnpm run devCargo is simpler!
Further Reading
Section titled “Further Reading”Official Documentation
Section titled “Official Documentation”- The Cargo Book - Complete guide
- Cargo Commands Reference
- Manifest Format (Cargo.toml)
Useful Tools
Section titled “Useful Tools”- cargo-edit -
cargo upgrade,cargo set-version(cargo add/cargo removeare now built into Cargo) - cargo-watch - Auto-rebuild on file change
- cargo-expand - Expand macros
- cargo-tree - View dependency tree (built-in)
Comparisons
Section titled “Comparisons”Exercises
Section titled “Exercises”Exercise 1: Create and Run a Project
Section titled “Exercise 1: Create and Run a Project”cargo new hello_cargocd hello_cargocargo runExercise 2: Add a Dependency
Section titled “Exercise 2: Add a Dependency”Add the rand crate and use it:
Cargo.toml:
[dependencies]rand = "0.9"src/main.rs:
use rand::Rng;
fn main() { let random_number = rand::rng().random_range(1..=100); println!("Random number: {random_number}");}Note: rand 0.9 renamed the old
thread_rng()torng()andgen_range()torandom_range()(becausegenbecame a reserved keyword in edition 2024). If you follow a rand 0.8 tutorial, those are the two calls to update.
Run:
cargo runExercise 3: Explore cargo Commands
Section titled “Exercise 3: Explore cargo Commands”cargo build # Buildcargo clean # Cleancargo doc --open # Generate and open docscargo tree # Show dependency treecargo --version # Show versioncargo --list # List all commandsExercise 4: Create a Library
Section titled “Exercise 4: Create a Library”cargo new my_lib --libcd my_libsrc/lib.rs:
pub fn add(a: i32, b: i32) -> i32 { a + b}
#[cfg(test)]mod tests { use super::*;
#[test] fn test_add() { assert_eq!(add(2, 3), 5); }}Run tests:
cargo testExercise 5: Format and Lint
Section titled “Exercise 5: Format and Lint”# Format codecargo fmt
# Check formattingcargo fmt --check
# Lintcargo clippySummary
Section titled “Summary”What you’ve learned:
- Cargo is the all-in-one Rust tool
- How to create projects (
cargo new) - How to build and run (
cargo build,cargo run) - How to manage dependencies (edit
Cargo.toml) - How to test, format, and lint
- Key differences from npm
Essential commands:
cargo new <project> # Create projectcargo run # Build and runcargo test # Run testscargo fmt # Format codecargo clippy # Lintcargo doc --open # Generate docscargo clean # Clean build artifactsCargo.toml structure:
[package]name = "my_project"version = "0.1.0"edition = "2024" # cargo new fills in the newest edition for you
[dependencies]# Add dependencies hereYou now understand Rust’s tooling!