Rust
Overview
Rust is a systems programming language that guarantees memory safety without a garbage collector. Its ownership model eliminates entire classes of bugs at compile time. This reference covers the core toolchain (cargo), web services with axum, WebAssembly with React frontends, and no_std patterns for bare-metal and kernel projects.
cargo workflow
Project commands
cargo is both the build system and package manager, so nearly every interaction with a Rust project starts here. These are the day-to-day commands: create projects, build, test, lint, format, and manage dependencies.
# Create a new project
cargo new myproject
cargo new --lib mylib # library crate
# Build and run
cargo build # debug build
cargo build --release # optimized build
cargo run # build + run
cargo run -- --port 8080 # pass args after --
# Testing
cargo test # run all tests
cargo test test_name # run a specific test
cargo test -- --nocapture # show println output during tests
# Code quality
cargo check # fast compile check (no codegen)
cargo fmt # format code
cargo fmt --check # check formatting (CI)
cargo clippy # lint
cargo clippy -- -D warnings # treat warnings as errors
# Dependencies
cargo add axum # add latest
cargo add axum@0.7 # add specific version
cargo add --dev tokio-test # add dev dependency
cargo update # update dependencies within semver
cargo update -p axum --precise 0.7.5 # pin a specific version
# Documentation
cargo doc --open # build and open docs in browser
cargo doc --no-deps # only your crate, skip dependencies
Targeting specific packages
Use -p <name> to scope any cargo command to a single crate — a workspace member or a dependency. The package name is the crate name from Cargo.toml, not the directory name. In workspaces, -p is the default way to avoid rebuilding the entire tree when you only care about one member.
# Type-check a single package (fast, no codegen)
cargo check -p my-crate
cargo check -p serde_json # works on dependencies too
# Build only one package
cargo build -p my-crate
cargo build -p axum --release
# Run only that package's tests
cargo test -p my-crate
cargo test -p serde_json -- --nocapture
# Lint a single package
cargo clippy -p my-crate
cargo clippy -p tokio -- -D warnings
# Clean a single package's artifacts
cargo clean -p my-crate
cargo clean -p reqwest && cargo build -p reqwest # force recompile one dep
If two dependencies share a name (rare), disambiguate with a full package-id spec: cargo check -p 'serde@1.0' or cargo check -p 'https://github.com/user/repo#0.4'.
Cleaning build artifacts
Remove stale artifacts when switching targets, profiles, or toolchains — cargo caches builds aggressively, and a stale target/ can cause confusing errors. cargo clean is also useful before a clean CI build or when disk space is tight.
# Full target directory wipe
cargo clean # removes target/ (debug + release + all profiles)
cargo clean --release # removes only target/release/
cargo clean --doc # removes only target/doc/
cargo clean --profile ci # removes a specific profile directory
# Workspace-aware cleaning
cargo clean --workspace # clean the entire workspace (default when no -p given)
cargo clean -p my-crate # clean only a specific workspace member
# Cross-compilation cleanup
cargo clean --target wasm32-unknown-unknown # clean for a specific target
cargo clean --target-dir /custom/build/dir # clean from a non-default target directory
# Safety & preview
cargo clean --dry-run # preview what would be deleted (add -v for file list)
When to reach for cargo clean:
| Scenario | Command |
|---|---|
| Switching from debug to release and getting weird errors | cargo clean |
After changing Cargo.toml build profiles | cargo clean |
| Before a fresh CI build | cargo clean && cargo build --release |
| Freeing up disk space without nuking everything | cargo clean --release then cargo build |
| Cross-compilation target change | cargo clean --target <triple> |
cargo clean only removes generated artifacts — it never touches your source or Cargo.lock. A full rebuild after cargo clean downloads nothing (crates are cached in ~/.cargo/registry), only recompiles.
Cargo.toml essentials
Cargo.toml is the single source of truth for a Rust project — package metadata, dependencies, and build settings. Dependency features (like serde's derive) opt into optional functionality, and the [profile.release] section tunes the size and speed of the production binary.
[package]
name = "myproject"
version = "0.1.0"
edition = "2021"
[dependencies]
tokio = { version = "1", features = ["full"] }
axum = "0.7"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sqlx = { version = "0.7", features = ["runtime-tokio", "postgres"] }
tracing = "0.1"
tracing-subscriber = "0.3"
[dev-dependencies]
tower-http = { version = "0.5", features = ["trace"] }
reqwest = "0.12"
[profile.release]
lto = true # link-time optimization
codegen-units = 1 # better optimization
opt-level = "s" # optimize for size
strip = true # strip symbols
Axum web framework
Minimal server
axum is built on top of tokio, so handlers are async and the whole app runs inside a #[tokio::main] runtime. Each handler returns something that implements IntoResponse — here JSON — and the Router maps URLs to handlers before being served on a TCP listener.
use axum::{Router, response::Json, routing::get};
use serde_json::{json, Value};
async fn root() -> Json<Value> {
Json(json!({ "message": "Hello, World" }))
}
async fn health() -> Json<Value> {
Json(json!({ "status": "ok" }))
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(root))
.route("/health", get(health));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
println!("Listening on http://0.0.0.0:3000");
axum::serve(listener, app).await.unwrap();
}
Structured routes with state
Real applications need shared state — here, a thread-safe in-memory user store behind a read/write lock. State injects that state into handlers, Path extracts URL parameters, and the Json extractor both parses request bodies and serializes responses.
use axum::{
Router,
extract::{State, Path},
http::StatusCode,
response::Json,
routing::{get, post},
};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
// Shared application state
#[derive(Clone)]
struct AppState {
db: Arc<tokio::sync::RwLock<Vec<User>>>,
}
#[derive(Clone, Serialize, Deserialize)]
struct User {
id: u64,
name: String,
email: String,
}
#[derive(Deserialize)]
struct CreateUser {
name: String,
email: String,
}
async fn list_users(State(state): State<AppState>) -> Json<Vec<User>> {
Json(state.db.read().await.clone())
}
async fn create_user(
State(state): State<AppState>,
Json(payload): Json<CreateUser>,
) -> (StatusCode, Json<User>) {
let mut db = state.db.write().await;
let user = User {
id: db.len() as u64 + 1,
name: payload.name,
email: payload.email,
};
db.push(user.clone());
(StatusCode::CREATED, Json(user))
}
async fn get_user(
State(state): State<AppState>,
Path(user_id): Path<u64>,
) -> Result<Json<User>, StatusCode> {
state
.db
.read()
.await
.iter()
.find(|u| u.id == user_id)
.cloned()
.map(Json)
.ok_or(StatusCode::NOT_FOUND)
}
#[tokio::main]
async fn main() {
let state = AppState {
db: Arc::new(tokio::sync::RwLock::new(Vec::new())),
};
let app = Router::new()
.route("/users", get(list_users).post(create_user))
.route("/users/{id}", get(get_user))
.with_state(state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
Middleware with tower
tower layers wrap every request that flows through the router, so they're ideal for cross-cutting concerns. ServiceBuilder composes layers in order — tracing, CORS, and a 30-second timeout — and a single .layer(middleware) applies the whole stack to the app.
use tower_http::{trace::TraceLayer, cors::CorsLayer};
use std::time::Duration;
use tower::ServiceBuilder;
let middleware = ServiceBuilder::new()
.layer(TraceLayer::new_for_http())
.layer(CorsLayer::permissive())
.timeout(Duration::from_secs(30));
let app = Router::new()
.route("/", get(root))
.layer(middleware);
Error handling
Define one app-wide error enum with thiserror for ergonomic error messages, then implement IntoResponse for it. Handlers can then return Result<T, AppError> directly, and failures become structured JSON responses with the right status code instead of panics.
use axum::response::{IntoResponse, Response};
use thiserror::Error;
#[derive(Error, Debug)]
enum AppError {
#[error("Not found: {0}")]
NotFound(String),
#[error("Bad request: {0}")]
BadRequest(String),
#[error("Internal error")]
Internal(#[from] anyhow::Error),
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let (status, message) = match self {
AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg),
AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg),
AppError::Internal(_) => (
StatusCode::INTERNAL_SERVER_ERROR,
"Internal server error".to_string(),
),
};
(status, Json(json!({ "error": message }))).into_response()
}
}
WASM with React frontend
Cargo.toml for WASM
Compiling Rust to WebAssembly requires a cdylib crate type, which produces the final .wasm module. wasm-bindgen is the bridge between Rust and JavaScript, and serde-wasm-bindgen converts serializable Rust types into native JS values.
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
wasm-bindgen = "0.2"
serde = { version = "1", features = ["derive"] }
serde-wasm-bindgen = "0.6"
Rust WASM module
Functions marked #[wasm_bindgen] become callable from JavaScript with no glue code. Returning a serializable struct instead of a bare number keeps the API richer — it arrives in JS as a plain object with typed fields.
use wasm_bindgen::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct ComputationResult {
pub result: f64,
pub iterations: u32,
}
#[wasm_bindgen]
pub fn compute_fibonacci(n: u32) -> ComputationResult {
let mut a = 0.0;
let mut b = 1.0;
for _ in 0..n {
let temp = a + b;
a = b;
b = temp;
}
ComputationResult {
result: b,
iterations: n,
}
}
#[wasm_bindgen]
pub fn process_data(json_input: &str) -> String {
let data: Vec<f64> = serde_json::from_str(json_input).unwrap_or_default();
let sum: f64 = data.iter().sum();
let mean = sum / data.len() as f64;
serde_json::to_string(&mean).unwrap()
}
Build and integrate with React
wasm-pack build --target web compiles the crate into a ready-to-import module. The generated bindings export an init() function that must be awaited once to load the wasm binary, plus one function per #[wasm_bindgen] export.
# Build WASM package
wasm-pack build --target web --out-dir ../www/src/wasm
# In React component:
# import init, { compute_fibonacci } from './wasm/my_wasm_lib';
// App.tsx
import init, { compute_fibonacci } from './wasm/my_wasm_lib';
import { useEffect, useState } from 'react';
function App() {
const [result, setResult] = useState<number | null>(null);
useEffect(() => {
init().then(() => {
const fib = compute_fibonacci(40);
setResult(fib.result);
});
}, []);
return <div>Fibonacci(40) = {result}</div>;
}
no_std for kernel / bare-metal projects
#![no_std] drops the standard library (which requires an OS) and leaves only the core crate, while #![no_main] replaces the runtime's entry point with your own _start function. Bare-metal code must also set panic = "abort" and provide a #[panic_handler], since there's no OS to clean up after a panic.
# Cargo.toml
[package]
name = "my-kernel"
version = "0.1.0"
edition = "2021"
[profile.dev]
panic = "abort"
[profile.release]
panic = "abort"
// main.rs — bare-metal entry point
#![no_std] // No standard library
#![no_main] // No standard main function
#![feature(asm_experimental_arch)]
use core::panic::PanicInfo;
// Entry point (never returns)
#[no_mangle]
pub extern "C" fn _start() -> ! {
// Set up the stack pointer, zero BSS, and init any hardware
loop {
// Kernel main loop
unsafe {
// Example: write to VGA text buffer
let vga_buffer = 0xb8000 as *mut u16;
vga_buffer.write_volatile(0x0f48); // 'H' in white on black
}
}
}
// Panic handler required for no_std
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {} // Halt the CPU — there is no OS to unwind to
}
Linker script (kernel.ld)
The linker script tells the linker where to place code and data in the final binary. This one loads the kernel at the conventional 1 MB mark (above x86 low memory) and groups the output into text, read-only data, data, and BSS sections.
ENTRY(_start)
SECTIONS {
. = 1M; /* Load kernel at 1MB */
.text : {
*(.text*)
}
.rodata : {
*(.rodata*)
}
.data : {
*(.data*)
}
.bss : {
*(.bss*)
}
}
Building for no_std targets
rustup target add installs a cross-compilation target — here ARM Cortex-M (thumbv7em-none-eabihf), a *-none-* target meaning "no host OS". For custom hardware, point cargo build --target at a JSON target spec file instead.
# Install target
rustup target add thumbv7em-none-eabihf
# Build
cargo build --target thumbv7em-none-eabihf --release
# For a custom target (use a target spec JSON file)
cargo build --target x86_64-unknown-none.json --release
See also
- Python Reference — FastAPI, uv, and scripting patterns
- Node.js & TypeScript — JavaScript/TypeScript runtime