Skip to content
ExponaBLOG
RUST PROGRAMMING · DAY 002 / 100

Setting up Rust: concepts, trade-offs and production lessons

rustup, Cargo and your first program.

Expona Admin7 min read

Getting started with Rust feels different from most languages you've used—not because it's harder, but because the tooling makes opinions explicit from day one. We'll walk through setting up Rust: concepts, trade-offs and production lessons in a way that mirrors how teams actually ship Rust in production, from the initial install through to catching bugs before they reach staging.

Why setup matters

Before writing a single line of Rust, you're making infrastructure decisions. The Rust compiler is strict about memory safety and ownership, so the setup experience—your editor, formatter, linter—either reinforces those guarantees or fights against them. We've seen teams spend weeks debugging issues that a properly configured rust-analyzer would have caught in seconds. Setup is not busywork; it's preventive maintenance.

Install: rustup brings the entire toolchain

Rust doesn't live in your system package manager in the way Node or Python do. Instead, you use rustup, which manages compiler versions, targets, and component installation. This matters in production: you can lock your team to a specific Rust version and reproducibly build the same binary on any machine.

Installation is a single command:

rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

This downloads a shell script that installs the stable Rust toolchain. On most systems, this takes under a minute. The script adds ~/.cargo/bin to your PATH, so after restarting your terminal, rustc and cargo are ready.

Verify the install with:

rust
rustc --version
cargo --version

You'll see something like rustc 1.75.0 and cargo 1.75.0. If you're on Windows, download the installer from rustup.rs instead—the Unix approach won't work natively (though WSL2 works fine).

Why rustup and not a system package?

rustup lets you manage multiple toolchains. You can test your code against nightly Rust, switch back to stable for production, and even override the version per project using a rust-toolchain.toml file. We've used this extensively in teams where one service needed an unstable feature while the rest of the codebase stayed on stable. You can't do that with apt install rustc.

Creating your first project

Cargo is Rust's build system and package manager, both combined. Create a project with:

rust
cargo new hello
cd hello
cargo run

Cargo scaffolds a complete project: a Cargo.toml manifest, a src/main.rs file, and a Git repo. Running cargo run compiles and executes in one step. You'll see:

rust
   Compiling hello v0.1.0
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.23s
     Running `target/debug/hello`
Hello, world!

The binary lives in target/debug/hello. For release builds, use cargo build --release, which compiles with optimizations and outputs to target/release/hello. Release binaries are significantly faster but take longer to compile—a trade-off you'll tune in your CI/CD pipeline.

Cargo: the backbone of Rust development

Cargo is far more than a build tool. It's your interface to the entire Rust workflow: dependency management, testing, documentation, and publishing. Understanding its structure and conventions prevents friction later.

Project structure

A Cargo project follows a standard layout:

File/Directory Purpose
Cargo.toml Project manifest: name, version, dependencies, metadata
Cargo.lock Locked versions for reproducible builds (commit to version control)
src/main.rs Entry point for binaries (must exist for binary crates)
src/lib.rs Entry point for libraries (optional, defines public API)
target/ Compiled artifacts (binaries, dependencies, intermediate files)

Dependencies and Cargo.toml

Add a dependency by editing Cargo.toml:

rust
[package]
name = "hello"
version = "0.1.0"
edition = "2021"

[dependencies]
serde = "1.0"
tokio = { version = "1.35", features = ["full"] }

Or use cargo add to do it automatically:

rust
cargo add serde
cargo add tokio --features full

Cargo fetches from crates.io by default. When you build, Cargo downloads the specified versions, resolves transitive dependencies, and generates Cargo.lock—a file that locks exact versions for reproducibility. For binary crates, commit Cargo.lock to version control. For libraries, many teams omit it (though it doesn't hurt).

Testing and common commands

Cargo has first-class testing support. Write tests inline:

rust
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_adds() {
        assert_eq!(add(2, 3), 5);
    }
}

Run tests with cargo test. Cargo compiles and runs all test functions in parallel. For integration tests (testing your library from the outside), create a tests/ directory at the project root. Each Rust file there becomes a separate test binary.

Other essential commands:

  • cargo build — Compile without running
  • cargo check — Type-check without generating binaries (faster for iteration)
  • cargo doc --open — Build and open HTML documentation from your code
  • cargo bench — Run benchmarks (requires a benches/ directory)
  • cargo publish — Publish to crates.io

In production, we often use cargo check in CI pipelines before full builds—it's 3–5x faster and catches most errors early.

Editor: rust-analyzer for real-time feedback

The Rust compiler's error messages are legendary for their clarity, but you shouldn't wait for compilation to catch mistakes. rust-analyzer is a language server that provides IDE-like features: completions, hover hints, go-to-definition, and real-time error squiggles.

Setup in VS Code

Install the official Rust Analyzer extension from the VS Code marketplace. That's it. It auto-detects your Rust project and starts providing hints immediately.

You'll see:

  • Hover over any variable or function to see its type
  • Completions suggest function names, method names, and imports
  • Red squiggles appear for borrowing errors or type mismatches before you compile
  • Quick fixes offer to add missing imports or insert semicolons

Configuration

For larger projects, you might want to configure rust-analyzer. Add this to your VS Code settings.json:

rust
"rust-analyzer.check.command": "clippy",
"rust-analyzer.check.features": "all",
"rust-analyzer.inlayHints.typeHints.enabled": true,
"rust-analyzer.inlayHints.parameterHints.enabled": true

This tells rust-analyzer to use clippy (the linter) for checks and to show inlay hints for types and parameters—invaluable when learning Rust.

For other editors: Neovim has lsp-zero and nvim-lspconfig, Sublime Text has LSP support via a plugin, and Emacs uses eglot or lsp-mode.

Toolchain: rustfmt and clippy

Rust's opinionated ecosystem includes two tools that ship with rustup: rustfmt (code formatter) and clippy (linter). Both enforce community standards and catch common mistakes.

rustfmt: consistent formatting

Format your code with:

rust
cargo fmt

This rewrites all Rust files to follow the standard style guide. Unlike languages where formatting is a religious debate, Rust formatting is settled. Your team doesn't discuss whether braces go on the next line—they just all run cargo fmt before committing.

Configure rustfmt with a rustfmt.toml in your project root:

rust
edition = "2021"
hard_tabs = false
max_width = 100
use_small_heuristics = "Default"

In CI, you can enforce formatting with cargo fmt -- --check, which fails if any file is not formatted.

clippy: the linter

Clippy catches idiomatic issues and potential bugs:

rust
cargo clippy

Common clippy warnings include suggesting iterator methods instead of explicit loops, pointing out unnecessary clones, and catching unreachable code. Many teams run clippy in CI and treat warnings as errors:

rust
cargo clippy -- -D warnings

The -D warnings flag turns warnings into errors, failing the build. This keeps code quality consistent across a team.

Typical CI workflow

In production, we chain these tools:

rust
cargo check
cargo fmt -- --check
cargo clippy -- -D warnings
cargo test
cargo build --release

This runs in under a minute for most projects and catches formatting issues, linting violations, type errors, and failing tests before you build the final binary.

Common mistakes

Not running cargo fmt before committing

Formatting drifts across PRs if you don't enforce it. Set up a pre-commit hook or require cargo fmt in your PR template. Many teams add this to their merge checks.

Ignoring clippy warnings

Clippy warnings often point to real inefficiencies or subtle bugs. If a warning is truly spurious, suppress it locally with #[allow(clippy::warning_name)], not everywhere. This keeps the signal-to-noise ratio high.

Committing target/ to version control

Your .gitignore should include target/. Cargo regenerates it, so there's no reason to version it. It bloats your repo and causes merge conflicts.

Not using Cargo.lock in binaries

If you're building a binary (service, CLI tool), commit Cargo.lock. This ensures your team and CI build identical artifacts. Libraries typically omit it because downstream users manage their own dependency versions.

Mixing stable and nightly features

Stick to stable Rust unless you have a specific reason for nightly (unstable syntax, performance features that aren't stabilized yet). Create a rust-toolchain.toml to lock your toolchain version explicitly:

rust
[toolchain]
channel = "stable"
components = ["rustfmt", "clippy"]

Key takeaways

  • Install Rust with rustup, not your system package manager. This gives you version control and component flexibility that production deployments need.
  • Cargo is far more than a build tool—it's your interface to testing, documentation, and the entire ecosystem. Understand Cargo.toml, Cargo.lock, and the standard project structure.
  • Set up rust-analyzer in your editor immediately. Real-time type hints and error squiggles catch ownership and borrowing mistakes before they reach the compiler.
  • Run cargo fmt and cargo clippy before every commit, or enforce them in CI. These tools remove entire categories of bugs and style disagreements.
  • Commit Cargo.lock for binaries to ensure reproducible builds across machines and CI. Libraries can omit it.
  • Use cargo check during development for fast feedback, and cargo test as your first line of defense. Build --release only when you're ready to measure real performance.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles