Rust Development Environment: Tooling, IDEs, and Workflow
Rust Development Environment: Tooling, IDEs, and Workflow
Rust's tooling is one of its strongest selling points. Cargo handles building, testing, dependency management, and publishing. Clippy catches common mistakes. rust-analyzer provides IDE intelligence that rivals mature ecosystems like Java. The ecosystem is well-integrated, which means less time configuring tools and more time writing code.
Installation and Setup
rustup
rustup manages Rust toolchains. Always install Rust through rustup, not your system package manager.
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Update Rust
rustup update
# Install nightly toolchain (for experimental features)
rustup toolchain install nightly
# Use nightly for a specific project
cd my-project && rustup override set nightly
# Install additional targets (cross-compilation)
rustup target add x86_64-unknown-linux-musl
rustup target add wasm32-unknown-unknown
rustup target add aarch64-apple-darwin
Essential Components
# Formatter
rustup component add rustfmt
# Linter
rustup component add clippy
# Source code for standard library (IDE navigation)
rustup component add rust-src
# Language server
rustup component add rust-analyzer
IDE Support
VS Code with rust-analyzer
rust-analyzer is the standard Rust language server. Install the "rust-analyzer" extension in VS Code.
Key features:
- Inlay type hints: Shows inferred types inline, which is invaluable for understanding complex generic code
- Go to implementation: Navigate to trait implementations
- Expand macro: See what a macro generates (hugely helpful for debugging proc macros)
- Code actions: Auto-import, generate implementations, convert between types
Useful settings:
// .vscode/settings.json
{
"rust-analyzer.check.command": "clippy",
"rust-analyzer.cargo.features": "all",
"rust-analyzer.inlayHints.parameterHints.enable": true,
"rust-analyzer.inlayHints.typeHints.enable": true,
"rust-analyzer.lens.references.abi.enable": true,
"rust-analyzer.procMacro.enable": true
}
Setting check.command to clippy means you get lint warnings directly in the editor, not just compiler errors.
JetBrains RustRover
RustRover is JetBrains' dedicated Rust IDE. It's free for non-commercial use and offers the standard JetBrains experience: excellent refactoring, debugging integration, and database tools.
VS Code vs RustRover: VS Code with rust-analyzer is lighter and faster for small-to-medium projects. RustRover is better for large projects, complex debugging, and developers already in the JetBrains ecosystem.
Cargo Tools
cargo-watch
Re-run commands when source files change:
cargo install cargo-watch
# Rebuild on save
cargo watch -x build
# Run tests on save
cargo watch -x test
# Run clippy on save
cargo watch -x clippy
# Chain commands
cargo watch -x clippy -x test -x "run -- --port 3000"
cargo-edit
Add, remove, and upgrade dependencies from the command line:
cargo install cargo-edit
# Add a dependency
cargo add serde --features derive
cargo add tokio --features full
# Add a dev dependency
cargo add --dev criterion
# Remove a dependency
cargo rm serde
# Upgrade dependencies
cargo upgrade
cargo-expand
See what macros generate:
cargo install cargo-expand
# Expand all macros in a file
cargo expand --lib
# Expand macros for a specific function
cargo expand my_module::my_function
This is essential for debugging derive macros and understanding what #[derive(Serialize)] or #[tokio::main] actually produce.
cargo-deny
Check dependencies for security vulnerabilities, license violations, and banned crates:
cargo install cargo-deny
# Initialize config
cargo deny init
# Run all checks
cargo deny check
# deny.toml
[advisories]
vulnerability = "deny"
unmaintained = "warn"
[licenses]
allow = ["MIT", "Apache-2.0", "BSD-2-Clause", "BSD-3-Clause", "ISC"]
confidence-threshold = 0.8
[bans]
multiple-versions = "warn"
cargo-nextest
A faster test runner that replaces cargo test:
cargo install cargo-nextest
# Run tests (with better output and parallelism)
cargo nextest run
# Run specific tests
cargo nextest run test_name
# Retry flaky tests
cargo nextest run --retries 2
nextest runs each test in its own process, which means better isolation and faster parallel execution. Test output is also cleaner — failures are shown at the end with clear formatting.
Debugging
LLDB / GDB
Rust compiles to native code, so standard debuggers work:
# VS Code: Install "CodeLLDB" extension, then use the debug panel
# Add a launch.json configuration:
{
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "launch",
"name": "Debug",
"cargo": {
"args": ["build", "--bin=my-app"],
"filter": { "kind": "bin" }
},
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug Tests",
"cargo": {
"args": ["test", "--no-run"],
"filter": { "kind": "lib" }
}
}
]
}
dbg! Macro
Rust's dbg! macro is the printf-debugging equivalent. It prints the expression, its value, and the file/line:
let x = 5;
let y = dbg!(x * 2); // Prints: [src/main.rs:3] x * 2 = 10
Unlike println!, dbg! returns the value, so you can insert it into expressions without restructuring code.
Common Patterns and Crates
Error Handling
# Cargo.toml
[dependencies]
anyhow = "1" # For application code (dynamic error types)
thiserror = "2" # For library code (derive Error trait)
// Application code: anyhow for convenience
use anyhow::{Context, Result};
fn read_config() -> Result<Config> {
let contents = std::fs::read_to_string("config.toml")
.context("Failed to read config file")?;
let config: Config = toml::from_str(&contents)
.context("Failed to parse config")?;
Ok(config)
}
// Library code: thiserror for typed errors
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ApiError {
#[error("Not found: {0}")]
NotFound(String),
#[error("Unauthorized")]
Unauthorized,
#[error("Database error: {0}")]
Database(#[from] sqlx::Error),
}
Serialization
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
Async Runtime
[dependencies]
tokio = { version = "1", features = ["full"] }
# Or for lighter-weight:
tokio = { version = "1", features = ["rt", "macros", "net"] }
HTTP
[dependencies]
reqwest = { version = "0.12", features = ["json"] } # Client
axum = "0.8" # Server
Compilation Speed
Rust compilation is famously slow. Here's what actually helps:
# .cargo/config.toml
# Use lld linker (much faster linking)
[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=lld"]
[target.x86_64-apple-darwin]
rustflags = ["-C", "link-arg=-fuse-ld=lld"]
[target.aarch64-apple-darwin]
rustflags = ["-C", "link-arg=-fuse-ld=lld"]
# Use cranelift backend in development (faster compilation, slower code)
# Requires nightly
[unstable]
codegen-backend = true
[profile.dev]
codegen-backend = "cranelift"
Biggest wins:
- mold/lld linker: Linking is often 50%+ of compile time. mold or lld cuts this dramatically.
- sccache: Shared compilation cache across projects:
cargo install sccache && export RUSTC_WRAPPER=sccache - Cranelift backend: Faster code generation at the cost of runtime performance. Perfect for development.
- Fewer dependencies: Every crate you add increases compile time. Audit your dependency tree.
Recommendations
New to Rust: VS Code + rust-analyzer + CodeLLDB. Install cargo-watch and cargo-edit. Use anyhow for error handling.
Productive setup: Add cargo-nextest (faster tests), cargo-deny (security), and configure lld/mold for faster builds.
Large projects: Consider RustRover, add sccache, and set up Cranelift for dev builds. Profile compile times with cargo build --timings.