Rust Ecosystem Crates and Project Templates

Reading time
11 min read
Word count
2007 words
Diagram count
1 diagram

Source: Victor Bona's Obsidian Compendium snapshot, Knowledge base/Rust/16 Rust Ecosystem Crates and Project Templates.md.

Purpose: provide a practical map for choosing Rust crates and project templates across libraries, CLIs, services, async services, no_std crates, FFI crates, and workspaces.

Rust Ecosystem Crates and Project Templates

Related notes: Rust, 00 Rust Mastery Roadmap, 04 Error Handling and API Design, 05 Modules Crates Cargo and Workspaces, 08 Async Rust Futures Tokio and Runtimes, 11 Testing Verification Benchmarking and Tooling, 13 Systems Programming Networking and IO, 14 FFI Embedded WebAssembly and Interop, Software Supply Chain Security.

Ecosystem selection model

The Rust ecosystem rewards deliberate dependency choices. A crate can save months, but it can also add unsafe code, transitive dependencies, build scripts, MSRV pressure, feature unification surprises, binary size, compile time, and supply-chain risk.

Rendering diagram...

Selection principle: choose the smallest maintained crate that expresses the domain well and leaves your public API stable. Do not add a framework to avoid understanding the boundary it owns.

Crate selection criteria

CriterionGood signalWarning signal
MaintenanceRecent releases, active issue triage, compatible Rust versions.Abandoned issues, no release path, unclear ownership.
API stabilitySemver discipline, changelog, deprecation path.Frequent breaking changes without migration notes.
Dependency treeSmall, understandable, feature-gated.Large tree for a minor helper.
Unsafe usageIsolated and documented.Unexplained unsafe in hot or exposed paths.
Build behaviorNo build script unless needed.Downloads code or probes host unpredictably.
LicenseCompatible with product and distribution.Missing, custom, or conflicting license.
MSRVStated minimum supported Rust version.Requires latest compiler without reason.
Target supportWorks on Linux, macOS, Windows, WASM, or embedded as needed.Hidden platform assumptions.
ObservabilityErrors and hooks integrate with your stack.Panics or stringly errors at boundaries.

Cargo inspection commands:

cargo tree
cargo tree -e features
cargo tree -i serde
cargo deny check
cargo audit
cargo geiger
cargo machete

Use cargo geiger as a pointer to unsafe review, not as a verdict. Use cargo machete to find unused dependencies, then confirm manually because feature-only usage can confuse tools.

Common crate map

DomainCommon cratesNotes
Errorsthiserror, anyhow, eyrethiserror for library errors, anyhow or eyre for binaries.
CLIclap, anstyle, indicatif, dialoguerKeep parsing separate from execution.
Configfigment, config, toml, serde_json; a reviewed maintained YAML adapter only when YAML is requiredValidate after parsing; serde_yaml itself is deprecated and archived.
Logging and tracingtracing, tracing-subscriber, tracing-opentelemetryPrefer spans over ad hoc logs.
Async runtimetokio, smol; async-std only for legacy compatibilityRuntime-bound I/O types and ecosystem compatibility matter.
HTTP serveraxum, actix-web, hyper, towerAxum plus Tower is a common default.
HTTP clientreqwest, hyper, ureqReuse client instances.
Serializationserde, serde_json, toml, postcard, ciborium, minicbor, prost, rkyvPick schema evolution policy early; use bincode 2 only for legacy compatibility.
Databasesqlx, diesel, SeaORM, rusqliteMatch abstraction to query complexity and runtime.
Testingproptest, quickcheck, insta, assert_cmd, wiremockAdd integration tests around boundaries.
Benchmarkingcriterion, iai-callgrindBenchmark public operations and hot paths.
Embeddedembedded-hal, heapless, defmt, embassy, rticFeature-gate host and target behavior.
FFIbindgen, cbindgen, cc, libloadingWrap raw APIs in safe layers.
WASMwasm-bindgen, wasm-pack, web-sys, js-sysBudget for boundary overhead and bundle size.

Library template

A Rust library should expose a small stable surface, precise errors, and minimal required features.

my-lib/
  Cargo.toml
  README.md
  src/
    lib.rs
    error.rs
    model.rs
    parser.rs
  tests/
    public_api.rs

Cargo.toml:

[package]
name = "my-lib"
version = "0.1.0"
edition = "2024"
rust-version = "1.85"
license = "MIT OR Apache-2.0"

[features]
default = ["std"]
std = ["serde?/std", "thiserror/std"]
serde = ["dep:serde"]

[dependencies]
serde = { version = "1", optional = true, default-features = false, features = ["derive"] }
thiserror = { version = "2", default-features = false }

src/lib.rs:

#![cfg_attr(not(feature = "std"), no_std)]
#![deny(missing_docs)]

mod error;
mod model;
mod parser;

pub use error::ParseError;
pub use model::Document;
pub use parser::parse_document;

Library guidance:

  • Public types are harder to change than private modules.
  • Avoid leaking dependency types unless the dependency is part of your intended API.
  • Use feature flags for optional serde, std, tracing, or integration support.
  • Add doc tests for the main happy path and one error path.
  • Keep unsafe private unless the caller must uphold a contract.

CLI template

CLIs need argument parsing, config layering, clear errors, exit codes, and testable command logic.

my-cli/
  Cargo.toml
  src/
    main.rs
    cli.rs
    config.rs
    command.rs
  tests/
    cli.rs
use clap::{Parser, Subcommand};
use std::path::PathBuf;

#[derive(Debug, Parser)]
#[command(name = "indexer")]
struct Cli {
    #[command(subcommand)]
    command: Command,
}

#[derive(Debug, Subcommand)]
enum Command {
    Scan {
        #[arg(long)]
        root: PathBuf,
    },
}

fn main() -> anyhow::Result<()> {
    let cli = Cli::parse();
    match cli.command {
        Command::Scan { root } => indexer::scan(root)?,
    }
    Ok(())
}

CLI production guidance:

ConcernGuidance
ErrorsHuman message on stderr, machine-readable option when needed.
Exit codesStable documented meanings for automation.
ConfigPrecedence order: flags, env, config file, defaults.
OutputSeparate user output from logs.
TestsUse assert_cmd for binary behavior.
ShellGenerate completions and man pages for serious tools.

Service template

Services need runtime ownership, config, state, routes, storage, observability, graceful shutdown, and deployment checks.

my-service/
  Cargo.toml
  src/
    main.rs
    config.rs
    error.rs
    http/
      mod.rs
      routes.rs
      state.rs
    storage/
      mod.rs
      postgres.rs
    telemetry.rs
  migrations/
  tests/
    health.rs
    api_contract.rs
use axum::{routing::get, Router};
use tokio::net::TcpListener;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    telemetry::init()?;
    let config = Config::from_env()?.validate()?;
    let state = AppState::connect(&config).await?;

    let app = Router::new()
        .route("/healthz", get(|| async { axum::http::StatusCode::NO_CONTENT }))
        .with_state(state);

    let listener = TcpListener::bind(&config.bind_addr).await?;
    axum::serve(listener, app)
        .with_graceful_shutdown(shutdown_signal())
        .await?;

    Ok(())
}

async fn shutdown_signal() {
    let _ = tokio::signal::ctrl_c().await;
}

Service checklist:

  • Config::validate rejects dangerous or incoherent settings.
  • State is clone-cheap and contains pools, clients, and handles.
  • Readiness checks dependencies needed to serve traffic.
  • Liveness checks process health without cascading dependency outages.
  • Shutdown stops ingress, cancels background tasks, drains important work, and closes pools.
  • Tracing spans include request ID, route, status, latency, and dependency calls.
  • Database migrations have deploy order and rollback posture.

Async service template

Async services need extra discipline around tasks, cancellation, queues, and blocking work.

use tokio::sync::{mpsc, oneshot};
use tokio::task::JoinSet;

struct Work {
    payload: String,
    reply: oneshot::Sender<anyhow::Result<()>>,
}

async fn supervise_workers(
    mut rx: mpsc::Receiver<Work>,
    shutdown: tokio_util::sync::CancellationToken,
    max_concurrency: usize,
) {
    assert!(max_concurrency > 0);
    let mut tasks = JoinSet::new();

    loop {
        while tasks.len() >= max_concurrency {
            observe_worker(tasks.join_next().await);
        }

        tokio::select! {
            _ = shutdown.cancelled() => {
                rx.close();
                break;
            }
            work = rx.recv() => match work {
                Some(work) => {
                    tasks.spawn(async move {
                        let result = process(work.payload).await;
                        let _ = work.reply.send(result);
                    });
                }
                None => break,
            },
            result = tasks.join_next(), if !tasks.is_empty() => {
                observe_worker(result);
            }
        }
    }

    tasks.abort_all();
    while let Some(result) = tasks.join_next().await {
        observe_worker(Some(result));
    }
}

fn observe_worker(result: Option<Result<(), tokio::task::JoinError>>) {
    if let Some(Err(err)) = result {
        tracing::error!(error = %err, "worker task failed");
    }
}

async fn process(_payload: String) -> anyhow::Result<()> {
    Ok(())
}

This supervisor deliberately stops accepting work, aborts active tasks, and rejects queued requests by dropping their reply senders during shutdown. A drain policy would stop producers, retain queued work, apply a deadline, and join every accepted task instead. State that choice in the service contract.

Async design rules:

RuleReason
Use bounded channels.Backpressure must exist somewhere.
Own every JoinHandle or use a supervised task set.Detached tasks hide failures.
Never hold blocking locks across .await.Can deadlock or starve the runtime.
Put CPU work on a blocking pool or Rayon.Async runtimes are for IO progress.
Add cancellation tokens for long-lived tasks.Shutdown must be coordinated.
Instrument queue depth and task failures.Saturation is otherwise invisible.

no_std crate template

no_std libraries should keep their minimal build honest in CI.

my-codec/
  Cargo.toml
  src/
    lib.rs
    error.rs
    encode.rs
    decode.rs
  tests/
    roundtrip.rs
[features]
default = ["std"]
std = ["alloc", "serde?/std"]
alloc = ["serde?/alloc"]
serde = ["dep:serde"]

[dependencies]
serde = { version = "1", optional = true, default-features = false, features = ["derive"] }
heapless = { version = "0.9", optional = true }
#![cfg_attr(not(feature = "std"), no_std)]

#[cfg(feature = "alloc")]
extern crate alloc;

pub fn encode_len(len: usize, out: &mut [u8]) -> Option<&[u8]> {
    if out.len() < 2 || len > u16::MAX as usize {
        return None;
    }
    out[0] = (len >> 8) as u8;
    out[1] = len as u8;
    Some(&out[..2])
}

CI checks:

cargo check --no-default-features
cargo check --no-default-features --features alloc
cargo check --all-features

For embedded targets, add target-specific cargo check once the target triple is known.

FFI crate template

FFI crates are usually split into raw bindings and safe wrappers.

foo-sys/
  Cargo.toml
  build.rs
  wrapper.h
  src/lib.rs
foo/
  Cargo.toml
  src/lib.rs

foo-sys responsibilities:

  • Link the native library.
  • Generate or include raw bindings.
  • Expose C-like symbols with minimal interpretation.
  • Avoid pretending raw pointers are safe.

foo responsibilities:

  • Own handles with Drop.
  • Convert errors into Rust error types.
  • Encode lifetimes and thread-safety.
  • Expose safe methods unless caller obligations remain.
pub struct LibraryHandle {
    raw: std::ptr::NonNull<foo_sys::foo_handle>,
}

// Safety: the native library documents that an owned handle may be moved to
// another thread, all access requires `&mut self`, and destruction is valid on
// whichever thread owns the handle. It does not permit concurrent shared use,
// so this wrapper intentionally does not implement `Sync`.
unsafe impl Send for LibraryHandle {}

impl Drop for LibraryHandle {
    fn drop(&mut self) {
        unsafe {
            // Safety: the wrapper uniquely owns a live handle returned by
            // `foo_create`, and Drop calls its matching destructor once.
            foo_sys::foo_destroy(self.raw.as_ptr());
        }
    }
}

Only implement Send or Sync after reading the C library's thread-safety contract. If the C library has global state or thread affinity, encode that in Rust rather than documenting around it.

Workspace layouts

Workspaces let you separate stable contracts from binaries and adapters.

platform/
  Cargo.toml
  crates/
    domain/
    protocol/
    storage/
    service/
    cli/
    ffi/
  xtask/
  migrations/
  deny.toml

Root Cargo.toml:

[workspace]
members = [
    "crates/domain",
    "crates/protocol",
    "crates/storage",
    "crates/service",
    "crates/cli",
    "xtask",
]
resolver = "3"

[workspace.package]
edition = "2024"
rust-version = "1.85"
license = "MIT OR Apache-2.0"

[workspace.dependencies]
anyhow = "1"
serde = { version = "1", features = ["derive"] }
thiserror = "2"
tracing = "0.1"

Workspace guidance:

CrateShould containShould avoid
domainCore types, invariants, pure behavior.Tokio, SQL, HTTP framework types.
protocolWire models, serde, compatibility tests.Business behavior.
storageDatabase queries and transactions.HTTP routing.
serviceRuntime wiring, routes, tasks.Reusable domain rules hidden in handlers.
cliArgument parsing and command orchestration.Library-only logic.
xtaskRepo automation in Rust.Production runtime dependencies.

Use workspace dependency inheritance to keep versions aligned. Use feature flags to prevent a low-level crate from pulling service dependencies into every build.

Project template decision table

Project typeDefault cratesTemplate priorityExtra review
Librarythiserror, optional serdeSmall public API and semver.MSRV, feature flags, docs.
CLIclap, anyhow, tracing or stderrClear UX and automation behavior.Exit codes and config precedence.
Sync serviceaxum can still run on Tokio; sync work isolated.Thread pools and blocking boundaries.Pool exhaustion and shutdown.
Async servicetokio, axum, tower, tracing, sqlxRuntime ownership and backpressure.Cancellation, queue bounds, timeouts.
no_std cratecore, optional alloc, heaplessMinimal features and target checks.Default features and panic assumptions.
FFI cratebindgen, cbindgen, cc, thiserrorABI stability and safe wrapper.Allocator ownership and unwind policy.
WASM packagewasm-bindgen, wasm-pack, web-sysBoundary shape and bundle size.Host APIs and panic hooks.

Ecosystem Snapshot and Freshness Policy

This note was reviewed on 2026-07-10 with Rust 1.96.0. Version lines shown in templates are compatibility ranges, not an instruction to skip lockfile review.

AreaReviewed line used hereMaintenance note
Core serializationSerde 1Stable ecosystem foundation; inspect adapter maintenance separately
Errorsthiserror 2, anyhow 1Keep typed errors in libraries and reports at application edges
Async and HTTPTokio 1, Axum 0.8, Hyper 1, Tower 0.5Align features and versions through the selected stack
BenchmarkingCriterion 0.8Requires a modern compiler; keep MSRV policy explicit
Property testsProptest 1Strategies and shrinking quality matter more than case count alone
Embedded containersHeapless 0.9Check target atomics, MSRV, and capacity behavior
C bindingsBindgen 0.72, cbindgen 0.29Pin Clang and header-generation environments
MetricsMetrics 0.24Recorder and exporter selection is a separate operational decision

Known status cautions:

  • The original serde_yaml repository is archived and its crate is deprecated. Select and audit a maintained adapter only when YAML is required.
  • Bincode 3.0 intentionally fails compilation to announce that the project is unmaintained. Pin existing bincode 2 consumers narrowly while migrating; do not select bincode for a new design.
  • The async-std maintainers discontinued the runtime and recommend migration to smol. Treat it as legacy compatibility only.
  • cargo-watch is in life-support or archived status. Prefer Bacon or Watchexec for new file-watching workflows.
  • A popular crate is not automatically the correct boundary. Recheck releases, repository activity, advisories, MSRV, licenses, build scripts, unsafe use, and target support at adoption time.

Dependency Decision Record

For every material runtime or build dependency, record:

Need:
Candidates and version lines:
Selected crate and enabled features:
Public API exposure:
Transitive, native, build-script, and unsafe surface:
MSRV and target evidence:
License and advisory evidence:
Exit or replacement strategy:
Owner and next review date:

Review the record when a major version changes, a maintainer archives the project, an advisory lands, the target matrix changes, or dependency types leak into more public API.

Workspace Policy Template

[workspace.lints.rust]
unsafe_code = "deny"

[workspace.lints.clippy]
unwrap_used = "warn"

[profile.release]
lto = "thin"
codegen-units = 1
debug = "line-tables-only"

Each member opts in with [lints] workspace = true. Change the unsafe policy at the narrow crate level when FFI or a low-level abstraction requires it, and require explicit safety review rather than weakening the entire workspace.

Ecosystem tradeoffs

Rust has several recurring ecosystem tradeoffs:

TradeoffPrefer A whenPrefer B when
Framework vs libraryYou need strong conventions and shared middleware.You need a small stable core and custom boundaries.
Explicit SQL vs ORMQuery behavior and performance must be obvious.CRUD velocity and relation helpers dominate.
anyhow vs custom errorsBinary edge or orchestration code.Library API or caller-actionable failures.
Sync vs asyncWork is CPU-bound, simple, or low concurrency.Many concurrent IO operations share a runtime.
std vs no_stdHost platform and ergonomics matter.Target portability or embedded constraints matter.
Generated code vs hand-writtenSchema is large and tool-owned.API is small and needs careful review.
Large crate vs small cratesIntegrated behavior reduces real complexity.You only need a narrow utility.

Dependency policy

Production Rust projects should encode dependency policy in the repository.

# deny.toml sketch
[advisories]
ignore = []

[licenses]
allow = ["MIT", "Apache-2.0", "BSD-3-Clause"]

[bans]
multiple-versions = "warn"
wildcards = "deny"

Policy should cover:

  • Licenses allowed for runtime and build dependencies.
  • Security advisory behavior.
  • Duplicate crate versions.
  • Git dependencies and path dependencies.
  • Build scripts and native linking.
  • Minimum supported Rust version.
  • Release profile settings.
  • Reproducible lockfile policy for applications.

Libraries often should not commit Cargo.lock unless they have a reason. Applications and services should commit it.

Review checklist

  • The crate selection has a written reason tied to the project boundary.
  • Public APIs do not leak dependencies accidentally.
  • Feature flags are additive and checked with --no-default-features and --all-features.
  • Dependency tree, duplicate versions, licenses, advisories, and unused dependencies are inspected.
  • The chosen template separates domain, IO, runtime, and adapter concerns.
  • CLI output, service routes, FFI ABI, WASM exports, and no_std features are treated as public contracts.
  • Async tasks have owners, cancellation, and bounded channels.
  • Database and HTTP crates match the runtime and operational model.
  • Workspace-level versions and lints reduce drift across crates.
  • CI runs format, clippy, tests, doc tests, feature checks, audit or deny, and target-specific checks.

Common mistakes

  • Adding a dependency for a few lines of simple code.
  • Hiding dependency types in public APIs and later being unable to upgrade.
  • Letting default features pull in std, TLS stacks, or async runtimes unexpectedly.
  • Using anyhow in a library where callers need structured recovery.
  • Creating a workspace where every crate depends on the service crate.
  • Skipping cargo tree -e features and missing feature unification.
  • Treating cargo audit as complete supply-chain review.
  • Making a no_std claim without --no-default-features CI.
  • Shipping FFI or WASM artifacts without testing from the foreign host.
  • Choosing a web framework before defining cancellation, backpressure, and state ownership.

Primary Sources