Your first project with Cargo
Let’s get Rust installed, set up your editor, and make, build and run a real project. Cargo, Rust’s all-in-one tool, does almost all of it.
RememberIf you’re wondering how to do something to a Rust project, the answer usually starts with cargo.
1. Install Rust
Section titled “1. Install Rust”Rust is installed with rustup, which also installs Cargo and lets you update everything with one command. On macOS or Linux, run this in a terminal and accept the defaults:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | shOn Windows, download and run rustup-init.exe from rustup.rs, which adds “You may also need the Visual Studio prerequisites”: rustup may ask you to install the Visual Studio C++ build tools first, and Rust programs need them to link. (On Linux and macOS a C linker, usually from gcc or Xcode’s command line tools, plays the same part.) Then open a new terminal and check it worked. The runs on this page use the versions below:
$ rustc --version rustc 1.99.1 (58cf8f9c3 2026-10-13) $ cargo --version cargo 1.99.1 (5f94df478 2026-08-27) $ cargo fmt --version rustfmt 1.10.0-stable (58cf8f9c35 2026-10-13) $ cargo clippy --version clippy 0.1.99 (58cf8f9c35 2026-10-13)
Later, rustup update moves you to the newest Rust. A new stable version comes out every six weeks, and the Book says “Each upgrade should be painless” (the Book: Stability Without Stagnation). rustup’s default profile also installs cargo fmt and cargo clippy for section 5 (rustup book: Profiles).
2. Set up your editor
Section titled “2. Set up your editor”In most editors the thing that matters is rust-analyzer, a language server: the program that gives an editor go to definition, completion, refactorings, and errors from rustc (and clippy, if you ask for it, below) (its manual). It also shows inlay hints (variable types as grey text) and can add a missing use line. Each editor gets it a little differently:
| Editor | What to do | Docs |
|---|---|---|
| VS Code | the rust-analyzer extension, which bundles the server. Uninstall the old, unmaintained “Rust” extension | rust-analyzer: VS Code, VS Code: Rust |
| JetBrains | RustRover, JetBrains’ own Rust IDE (separate from rust-analyzer’s docs); it detects your toolchain or offers to download rustup | RustRover quick start |
| Zed | Rust support is built in. It uses a rust-analyzer found on your PATH, or else installs its own |
Zed: Rust |
| Helix | rust-analyzer is its default Rust language server; its docs say to set up the server first |
Helix: language support |
| Neovim | built-in LSP client plus nvim-lspconfig; the manual’s one line is vim.lsp.enable('rust_analyzer') |
rust-analyzer: Vim/Neovim |
Editors that use rust-analyzer, other than VS Code and Zed (which can fetch their own), need the binary on your machine; rustup can install it with rustup component add rust-analyzer (the Profiles page above says IDE tools are added manually with rustup component add), and rust-analyzer’s own Helix notes say “it won’t install rust-analyzer automatically”. No editor was run for this page: each row follows the linked official page.
Two settings are worth finding:
- Format on save. VS Code: turn on Editor: Format On Save; the Rust formatter is
rustfmt, the same onecargo fmtruns. RustRover: Settings | Rust | Rustfmt, tick Use Rustfmt instead of the built-in formatter, then Configure actions on save and tick Reformat code (RustRover: Rustfmt). - Clippy hints inline. rust-analyzer runs
cargo checkon save by default (checkOnSaveandcheck.command, its configuration page). In VS Codesettings.json,"rust-analyzer.check.command": "clippy"makes it run clippy instead (rust-analyzer: Diagnostics).
3. Make a project
Section titled “3. Make a project”$ cargo new hello Creating binary (application) `hello` package note: see more `Cargo.toml` keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html $ ls -A hello .git .gitignore Cargo.toml src
Here’s what you get. The .git/ folder is there because cargo new starts a git repository unless you are already inside one (cargo new):
hello/ ├── .git/ a fresh git repository ├── src/ │ └── main.rs your program starts here ├── .gitignore already ignores target/ └── Cargo.toml your project's settings and dependencies
Cargo.toml describes your project and its dependencies, like package.json, go.mod or pyproject.toml:
[package]name = "hello"version = "0.1.0"edition = "2024"
[dependencies]And src/main.rs is your program. cargo new makes a binary project, and main in this file is where it starts. The ! in println! marks a macro (Macros, attributes and comments):
fn main() { println!("Hello, world!");}4. Build and run
Section titled “4. Build and run”These are the commands you’ll type all day. Here’s real output from each one (build times are shown as [time]s because they differ on every machine):
$ cargo run Compiling hello v0.1.0 (/home/you/hello) Finished `dev` profile [unoptimized + debuginfo] target(s) in [time]s Running `target/debug/hello` Hello, world! $ cargo check Checking hello v0.1.0 (/home/you/hello) Finished `dev` profile [unoptimized + debuginfo] target(s) in [time]s $ cargo build --release Compiling hello v0.1.0 (/home/you/hello) Finished `release` profile [optimized] target(s) in [time]s $ ./target/release/hello Hello, world!
cargo runbuilds a debug version and runs it.cargo checkonly checks your code for errors, without producing a program. The Book says it is often much faster than a build, and it’s what rust-analyzer runs when you save (section 2).cargo build --releasemakes an optimised program intarget/release/. That’s the one you ship or benchmark.
After the first build, two new things appear:
hello/ ├── .git/ ├── src/ │ └── main.rs ├── target/ new: everything Cargo builds │ └── debug/ │ └── hello your program (plus build caches) ├── .gitignore ├── Cargo.lock new: exact versions, commit it └── Cargo.toml
target/holds everything Cargo builds, including your program attarget/debug/hello. It can get big. It’s already in.gitignore, and Cargo rebuilds what it needs if you delete it (cargo cleandoes that, docs).Cargo.lockrecords the exact version of every library you use. For a program like this one, commit it, so everyone builds the same thing (Cargo.toml vs Cargo.lock).
5. Keep it tidy: fmt and clippy
Section titled “5. Keep it tidy: fmt and clippy”cargo fmt formats your code in the community’s standard style, so style is one less thing to argue about in code review. Here is a badly laid-out file. --check lists what would change and exits with an error; plain cargo fmt rewrites the file:
use std::collections::HashMap;fn main(){let mut stock=HashMap::new(); stock.insert("apple",3);stock.insert("pear",5); let total:i32=stock.values().sum(); if total>7{println!("{total} items, no restock")}else{println!("restock"); }}$ cargo fmt --check Diff in /home/you/hello/src/main.rs:1: use std::collections::HashMap; -fn main(){ -let mut stock=HashMap::new(); - stock.insert("apple",3);stock.insert("pear",5); - let total:i32=stock.values().sum(); - if total>7{println!("{total} items, no restock")}else{ -println!("restock"); +fn main() { + let mut stock = HashMap::new(); + stock.insert("apple", 3); + stock.insert("pear", 5); + let total: i32 = stock.values().sum(); + if total > 7 { + println!("{total} items, no restock") + } else { + println!("restock"); } } $ cargo fmt $ cat src/main.rs use std::collections::HashMap; fn main() { let mut stock = HashMap::new(); stock.insert("apple", 3); stock.insert("pear", 5); let total: i32 = stock.values().sum(); if total > 7 { println!("{total} items, no restock") } else { println!("restock"); } } $ cargo fmt --check
cargo clippy is a linter: its README says it has over 800 lints. It points out code that works but could be simpler or clearer, and tells you how. This file compiles, and clippy still has two remarks:
fn main() { let names: Vec<String> = std::env::args().skip(1).collect(); let done = names.len() > 2; if done == true { println!("done"); } if names.len() == 0 { println!("no names"); }}$ cargo clippy
Checking hello v0.1.0 (/home/you/hello)
warning: equality checks against true are unnecessary
--> src/main.rs:4:8
|
4 | if done == true {
| ^^^^^^^^^^^^ help: try: `done`
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.99.0/index.html#bool_comparison
= note: `#[warn(clippy::bool_comparison)]` on by default
warning: length comparison to zero
--> src/main.rs:7:8
|
7 | if names.len() == 0 {
| ^^^^^^^^^^^^^^^^ help: using `is_empty` is clearer and more explicit: `names.is_empty()`
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.99.0/index.html#len_zero
= note: `#[warn(clippy::len_zero)]` on by default
warning: `hello` (bin "hello") generated 2 warnings (run `cargo clippy --fix --bin "hello" -p hello -- ` to apply 2 suggestions)
Finished `dev` profile [unoptimized + debuginfo] target(s) in [time]s
Run both before you commit, or let your editor do it (section 2). Macros, attributes and comments runs both on snippets.
Try it yourself
Section titled “Try it yourself”-
Greet someone by name. Replace
src/main.rswith this, then runcargo runandcargo run -- Ferris.|| "world".to_string()is a small function that only runs if there is no name. Everything after--goes to your program, andnth(1)skips the first item, which the docs say is traditionally the program’s own path. With no name it printsHello, world!; with one it printsHello, Ferris!:Rustsrc/main.rsfn main() {let name = std::env::args().nth(1).unwrap_or_else(|| "world".to_string());println!("Hello, {name}!");}terminal$ cargo run Hello, world! $ cargo run -- Ferris Hello, Ferris!
-
Break it on purpose. Change the first line of
maintolet count: i32 = "three";(leave theprintln!under it) and runcargo check. The compiler stops withE0308, points at the string, and says it expectedi32. Nothing ran:$ cargo checkerror[E0308]: mismatched types --> src/main.rs:2:22 | 2 | let count: i32 = "three"; | --- ^^^^^^^ expected `i32`, found `&str` | | | expected due to this
-
Let clippy fix its own remarks. Paste the clumsy file from section 5 into
src/main.rsof a fresh project and runcargo clippy --fix. Cargo refuses, because a project made bycargo newhas uncommitted files and a fix edits them. Commit first, or add--allow-dirty. Clippy reports “Fixed src/main.rs (2 fixes)”, and the file now saysif doneandnames.is_empty():terminal$ cargo clippy --fix error: the working directory of this package has uncommitted changes, and `cargo fix` can potentially perform destructive changes; if you'd like to suppress this error pass `--allow-dirty`, or commit the changes to these files: * .gitignore (dirty) * Cargo.toml (dirty) * src/ (dirty) $ cargo clippy --fix --allow-dirty Checking hello v0.1.0 (/home/you/hello) Fixed src/main.rs (2 fixes) Finished `dev` profile [unoptimized + debuginfo] target(s) in [time]s $ cat src/main.rs fn main() { let names: Vec<String> = std::env::args().skip(1).collect(); let done = names.len() > 2; if done { println!("done"); } if names.is_empty() { println!("no names"); } }
The same jobs in your language
Section titled “The same jobs in your language”| Language | New project | Run | Check only | Release build | Test | Format | Lint |
|---|---|---|---|---|---|---|---|
| Rust | cargo new |
cargo run |
cargo check |
cargo build --release |
cargo test |
cargo fmt |
cargo clippy |
| Go | go mod init (you write main.go) |
go run . |
go vet ./... |
go build (one default mode) |
go test ./... |
gofmt |
go vet, staticcheck |
| Python | uv init |
python3 main.py |
python3 -m py_compile (syntax only); mypy for types |
nothing to compile | python3 -m unittest, pytest |
ruff format |
ruff check |
| TypeScript | npm init |
node main.ts (Node 22.18 or newer) |
tsc --noEmit |
tsc |
node --test, vitest |
prettier |
eslint |
| Java | gradle init |
java Main.java, gradle run |
javac (compiling is the check) |
gradle build |
gradle test |
google-java-format, spotless |
checkstyle, Error Prone |
| Kotlin | gradle init |
gradle run |
kotlinc (compiling is the check) |
gradle build |
gradle test |
ktlint |
detekt |
| C# | dotnet new console |
dotnet run |
dotnet build |
dotnet publish -c Release |
dotnet test |
dotnet format |
analysers, which run in dotnet build |
| C | write a Makefile or CMakeLists.txt |
build, then run the binary | gcc -fsyntax-only |
gcc -O2 |
ctest |
clang-format |
clang-tidy |
| C++ | write a Makefile or CMakeLists.txt |
build, then run the binary | g++ -fsyntax-only |
g++ -O2 |
ctest |
clang-format |
clang-tidy |
Many cells are separate tools, not part of the language’s own toolchain (uv, mypy, pytest, ruff, vitest, prettier, eslint, staticcheck, Gradle, ktlint, detekt, clang-*). The image has Go, Node, Java, Kotlin, Python, .NET and GCC but not those, so their cells are the usual names, not runs.
The runs behind the table
$ go mod init example.com/hello # Go go: creating new go.mod: module example.com/hello go: to add module requirements and sums: go mod tidy $ go run . # Go Hello, world! $ gofmt -l . # Go: lists files whose layout differs (none) $ go vet ./... # Go: type-checks and runs analysers $ go test ./... # Go ? example.com/hello [no test files] $ node main.ts # TypeScript, run directly by Node Hello, world! $ tsc --noEmit main.ts # TypeScript: checks, writes no files $ java Main.java # Java: compiles and runs one file Hello, world! $ javac Main.java # Java: compiling is the check $ kotlinc Main.kt # Kotlin: compiling is the check $ python3 -m py_compile main.py # Python: syntax only $ gcc -fsyntax-only main.c # C: checks, writes no program $ g++ -fsyntax-only main.cpp # C++: checks, writes no program $ dotnet new console -o app # C# The template "Console App" was created successfully. $ dotnet run --project app # C# Hello, World! $ dotnet format app --verify-no-changes # C#
Most of the checks (go vet, gofmt -l, tsc --noEmit, javac, kotlinc, py_compile, the two -fsyntax-only runs and dotnet format) print nothing when they find nothing wrong. Not run: Gradle, uv, ruff, mypy, pytest, vitest, prettier, eslint, ktlint, detekt, clang-format, clang-tidy, ctest, dotnet build and dotnet test.
Read more
Section titled “Read more”- The Rust Book, chapter 1.3: Hello, Cargo! and Appendix D: Useful development tools (rustfmt,
cargo fix, Clippy, rust-analyzer) - The Cargo Book: Creating a new package and
cargo fix - Clippy documentation, the list of lints and rustfmt
Where to next
Section titled “Where to next”- Need a library? Finding and using crates.
- Program getting long? Splitting code into files, then the Modules lesson for the full set of rules.
- Ready for the language itself? Start with Hello World.
Paths in Cargo’s output are shown as /home/you.
Learn Rust is an independent project, not affiliated with the Rust Project or the Rust Foundation. Code and text are under the MIT licence.Source on GitHub.