← Rust: Zero to Depth
Full-stack DevRust: Zero to Depth

Ecosystem Selection and the Voyage: From cargo new to Rustacean

Part 14 of the Rust: Zero to Depth series (the finale): the de facto standards of the starter stack, the thiserror/anyhow dividing line proven in code, a five-signal due-diligence method for reading a crate, a full look back across fourteen parts, and the road ahead for a Rustacean.

RustEcosystemCratesthiserroranyhowCareer

Fourteen parts ago, you cargo newed your first project; now ownership, the type system, concurrency, unsafe, macros, and performance engineering all live in your toolbox. The last piece of the puzzle isn’t the language — Rust’s philosophy of “mechanism from the language, implementation from the ecosystem” makes selection a daily skill: where the standard library deliberately leaves blanks (executors, web, serialization), two hundred thousand crates on crates.io fill in. The finale covers three things: the de facto standards of the starter stack, a due-diligence method for judging whether a crate deserves your dependency, and a look back across the full arc of fourteen parts — then it sees you out of the harbor.

The starter stack: de facto standards by domain

Ecosystems shift, but the skeleton is remarkably stable. These are the defaults — deviating from them requires a reason, not the other way around:

  • tokio — the de facto async runtime (the industrial version of Part 10’s executor). Choosing it is like choosing Linux: the niche is the moat.
  • axum — the web framework from the tokio team itself: routes, extractors, and middleware fully typed, meshing seamlessly with Parts 5 and 7’s traits and generics.
  • serde — a synonym for serialization. Part 12’s derive principle, magnified ten-thousandfold, is this.
  • clap — CLI argument parsing: declare your command-line interface as a struct, derive the entire parser.
  • thiserror / anyhow — the two halves of error handling; the next section repays that debt in full.
  • tracing — structured logging and spans, async-aware; the grown-up version of println! debugging.
  • criterion / proptest — benchmarking (Part 13) and property-based testing: the latter auto-generates hundreds of cases to assault your function — a relative of Part 4’s exhaustiveness spirit in the testing world.

Learn the shape of each: what problem it solves, what its core abstraction is. Look up specific APIs in the docs — Part 1 said it: knowing where the answers live is half of learning.

Repaying Part 4’s debt: the thiserror/anyhow dividing line

Part 4 laid down a rule: “libraries use structured errors; applications use aggregate errors,” with a dividing line of one question — does your caller need to distinguish your errors? Now watch these two crates turn the rule into zero boilerplate.

The library side, thiserror: you write the enum and the semantics; it generates all of Display, Error, and From:

#[derive(thiserror::Error, Debug)]
pub enum ConfigError {
    #[error("failed to read {path}")]
    Io { path: String, #[source] source: std::io::Error },
    #[error("invalid port {0}: must be 0..=65535")]
    BadPort(u32),
}

fn parse_port(raw: &str) -> Result<u16, ConfigError> {
    let n: u32 = raw.parse().map_err(|_| ConfigError::BadPort(u32::MAX))?;
    u16::try_from(n).map_err(|_| ConfigError::BadPort(n))
}

The interpolation inside #[error("...")] references fields directly, and #[source] chains the causes — the caller gets a precise error they can match (“BadPort → ask the user to fix the config; Io → retry”), which is exactly what “structured” means. Measured output: invalid port 99999: must be 0..=65535.

The application side, anyhow: one anyhow::Result<T> catches everything, .context() annotates along the way, and {:#} prints the whole causal chain:

fn load_config() -> anyhow::Result<u16> {
    let raw = std::fs::read_to_string("port.txt")
        .map_err(|e| ConfigError::Io { path: "port.txt".into(), source: e })?;
    let port = parse_port(raw.trim())?;
    Ok(port)
}
// on failure, prints: failed to read port.txt: No such file or directory (os error 2)

Note the two sides coexist peacefully: library code inside your application stays precise with thiserror, while the top level aggregates with anyhow — the dividing line isn’t the project boundary, it’s “who is the audience of this error.”

How to read a crate: five due-diligence signals

Adding a dependency is hiring a long-term roommate — it moves into your build, your dependency tree, your supply chain. Download counts are the entry ticket (they prove popularity, not health); the hire is decided by five signals:

  1. Pulse: when was the last commit, the last release? Alive doesn’t mean frequently updated — a mature small crate untouched for years may be “finished,” not “dead”; the way to tell is whether anyone answers in the issue tracker.
  2. Response: are issues and PRs being handled? Maintainer bandwidth is a crate’s scarcest resource.
  3. Weight: cargo tree (Part 8) — how many crates move in with it? A TOML-parsing library dragging in 80 dependencies is the kind of weight a supply-chain review will choke on.
  4. Docs: is the docs.rs front page the README? Do the examples compile? (They do — Part 8’s doc tests guarantee it.) Documentation is a biopsy of how the maintainer treats users.
  5. Surface: how much unsafe and where (is the boundary drawn right, with Part 11’s eyes)? Is the license compatible with your project? Does the MSRV (minimum supported Rust version) fit your toolchain?

Five minutes of due diligence saves two weeks of dependency replacement half a year later.

Looking back: fourteen parts, one invariant

From Part 1 to here, the surface shows fourteen topics, but underneath there’s only one sentence repeating itself — the one that deserves to be carved on the series’ tombstone: move the invariant from the comment into the type, and let the compiler remember for you. Ownership is what it looks like on memory (Parts 2–3); enums and exhaustive matching, on domains (Part 4); traits and generics, on behavior (Parts 5, 7); smart pointers are its price list (Part 6); Cargo and modules, its engineering form (Part 8); fearless concurrency is its most glorious victory (Parts 9–10); unsafe and macros are the boundaries it honestly marks (Parts 11–12); performance engineering is its itemized bill (Part 13). The moment the borrow checker turned from adversary into mentor was the moment this invariant grew into your hands.

Where next? no_std embedded (ownership is just as sharp on bare metal), WebAssembly (Part 1’s “growing share of workloads”), kernels and drivers — or simply back to your own project: write a small service, a CLI, a parser; ship it, maintain it. The exercise book ends here; the rest of the road is written in code.

The last set of exercises, and the voyage

  1. Graduation project: a todo CLI — clap parses subcommands (add/list/done), serde persists tasks as JSON, anyhow aggregates errors, tracing logs. Under two hundred lines, but those two hundred lines contain the whole series.
  2. Due-diligence practice: pick three crates you’re about to use, run the five signals on each, write five lines of conclusion per crate. You’ll find one that doesn’t deserve its download count — that’s the moment the method proves itself.
  3. Read source: open the source of itertools or bytes and read for an hour. Wherever you get lost, return to the matching part — every part of this series was written so that real code reads clear.

The series closes here. Thank you for reading all fourteen parts — now go write that service that never pages anyone at 3 A.M.

guest@swangnice:~$