Skip to content
OpenSmartRoute
Skillv1.0.0

rust

Use when writing, reviewing, testing, or shipping Rust — ownership and the borrow checker (move/borrow/clone, Arc/RefCell, lifetimes), errors with Result/`?`/thiserror/anyhow, async on tokio, axum 0.8

by ericrisco(0) 0 installs
Free
Sign in to install

Free account. Installing gives you the manifest plus copy-paste snippets.

See reviews

About

Imported from ericrisco/rsc-harness (skills/rust/SKILL.md). Install upstream with npx skills add ericrisco/rsc-harness --skill rust. Copyright stays with the author.

Idiomatic Rust services

Write, review, test, and ship idiomatic async Rust services with the ownership model working for you, not against you.

Targets Rust 1.85+ / edition 2024 as the floor: native async fn in traits (no reflexive #[async_trait]), tokio 1.x as the runtime, axum 0.8 for the HTTP surface ({id} path-capture syntax, async-trait-free extractors), thiserror 2 for library error enums and anyhow at the application edge, sqlx for compile-time-checked SQL, and tracing for structured observability.

The thing an agent gets wrong in Rust is almost never syntax — it is ownership. Most "bugs" are compile errors about moves, borrows, and Send + Sync across .await. Front-load that mental model; the rest follows.

Ownership & borrowing (essentials)

This is the skill's center of gravity. Three moves: move (transfer ownership), borrow (&/&mut, no transfer), clone (a real copy, real cost) — and prefer them in that order, borrow first. Take &str/&[T] in function params, return owned String/Vec<T>: borrow on the way in, own on the way out is both the most flexible and the cheapest.

fn print_name(name: &str) { println!("{name}"); }   // borrows; caller keeps ownership

let s = String::from("ada");
print_name(&s);                                       // Good: lend a reference
println!("{s}");                                      // still usable

// Bad: takes by value, moves it, then the caller can't use `s` anymore.
fn consume(name: String) { /* ... */ }
consume(s);
// println!("{s}");  // error[E0382]: borrow of moved value: `s`

The four borrow-checker errors you will actually hit, with the fix:

// 1. "value moved here" (E0382): you used a value after moving it.
//    Fix: borrow instead of move, or .clone() only if you genuinely need two owners.
let v = vec![1, 2, 3];
let first = &v[0];           // Good: borrow
// let taken = v; let _ = first;  // Bad: moves v while `first` borrows it.

// 2. "cannot borrow as mutable more than once" (E0499): two &mut alive at once.
//    Fix: scope the first borrow so it ends before the second begins.
let mut data = vec![1, 2, 3];
{ let a = &mut data; a.push(4); }   // borrow ends here
let b = &mut data; b.push(5);       // Good: non-overlapping

// 3. "cannot borrow as mutable, already borrowed as immutable" (E0502).
//    Fix: don't hold a shared ref across a mutation; collect indices first, mutate after.

// 4. "does not live long enough" (E0597): a reference outlives the value it points to.
//    Fix: return an owned value, or restructure so the owner outlives the borrow.

Shared state: pick the smallest tool that fits. Decision table —

Need Use Why
One owner, sized value the value, or Box<T> Box only when heap/indirection/dyn is required
Shared ownership, single thread Rc<T> cheap refcount, not thread-safe
Shared ownership, across threads/await Arc<T> atomic refcount; the default for async app state
Interior mutability, single thread RefCell<T> runtime borrow check; panics on violation
Shared mutable state, async Arc<Mutex<T>> (tokio's) but prefer a channel if it is really message passing
Read-heavy shared state Arc<RwLock<T>> many readers, rare writer

Shared async state is Arc<AppState> injected through axum State — never a global static mut. Lifetimes, 'static, Cow, and the full smart-pointer tree -> references/ownership.md.

Errors

Error modeling is owned here, in Rust terms — it is not a separate skill. The model: Result<T, E> + ?, typed enums for libraries, anyhow at the edge, one mapping from a domain enum to an HTTP status.

use thiserror::Error;

// Library / domain layer: a typed enum callers can match on. #[from] gives free `?` conversion.
#[derive(Debug, Error)]
pub enum UserError {
    #[error("user {0} not found")]
    NotFound(i64),
    #[error("database error")]
    Db(#[from] sqlx::Error),   // any sqlx::Error becomes UserError::Db via `?`
}
// Application edge: anyhow when you just need context, not a match.
use anyhow::Context;
let config = std::fs::read_to_string(path)
    .with_context(|| format!("reading config at {path}"))?;   // adds a human breadcrumb

The 3-layer flow (twin of go's). Repository returns the typed domain error; service propagates with ?; the handler maps the enum to a status once, via IntoResponse — pattern-match the variant, never string-match the message, and log only the unexpected one (no internal leak to the client).

use axum::{http::StatusCode, response::{IntoResponse, Response}, Json};
use serde_json::json;

impl IntoResponse for UserError {
    fn into_response(self) -> Response {
        let status = match self {
            UserError::NotFound(_) => StatusCode::NOT_FOUND,            // 404
            UserError::Db(ref e) => {                                  // 500
                tracing::error!(error = %e, "unexpected db error");    // log here, not to the client
                StatusCode::INTERNAL_SERVER_ERROR
            }
        };
        (status, Json(json!({ "error": self.to_string() }))).into_response()
    }
}

Full repo->service->handler skeleton -> references/axum-service.md.

Async (tokio, essentials)

#[tokio::main] boots the multi-thread runtime; futures do nothing until .await. Bound your fan-out:

use tokio::task::JoinSet;

let mut set = JoinSet::new();
for id in ids {                                  // Good: a JoinSet you can drain and cap
    set.spawn(async move { fetch(id).await });
}
let mut out = Vec::new();
while let Some(res) = set.join_next().await {
    out.push(res??);                             // join error, then task error
}

The two pitfalls that bite agents, with the fix:

// Bad: std Mutex guard held across .await -> "future cannot be sent between threads safely".
let guard = state.lock().unwrap();
do_io().await;                  // guard is still alive here -> not Send
guard.update();

// Good: drop the lock before awaiting, or use tokio::sync::Mutex if the lock must span the await.
{
    let mut g = state.lock().unwrap();
    g.update();
}                               // guard dropped here
do_io().await;                  // nothing non-Send is held across the await
// Bad: a CPU-bound parse on the async worker thread starves every other task.
let parsed = heavy_parse(&blob);          // blocks the executor
// Good: move blocking/CPU work off the runtime.
let parsed = tokio::task::spawn_blocking(move || heavy_parse(&blob)).await?;

select! races futures (handle a cancellation token in one arm); tokio::sync::mpsc for message passing — prefer a channel over Arc<Mutex<T>> when the data flows one way. Cancellation, a bounded-concurrency + jittered-retry helper (ctx-aware, never retries a 4xx), and the full Send + Sync rules -> references/async-tokio.md.

Service (axum, essentials)

axum 0.8: {id} capture in the path, Path/State/Json extractors, your error enum as the return:

use axum::{extract::{Path, State}, routing::get, Router, Json};
use std::sync::Arc;

async fn get_user(
    State(app): State<Arc<AppState>>,            // shared state, not a global
    Path(id): Path<i64>,                         // {id} parsed and typed
) -> Result<Json<User>, UserError> {             // UserError: IntoResponse maps it
    let user = app.users.find(id).await?;        // `?` propagates the typed error
    Ok(Json(user))
}

let app = Router::new()
    .route("/users/{id}", get(get_user))         // 0.8 syntax: {id}, not :id
    .with_state(state);

Validate at the boundary and parse into a typed domain model — "parse, don't validate" makes illegal states unrepresentable, so the handler body never re-checks. Full skeleton — tower middleware (TraceLayer, timeout, request-id), graceful shutdown via axum::serve(...).with_graceful_shutdown(...), and JSON helpers -> references/axum-service.md.

Project layout

Keep the binary thin; put logic in the library so tests and integration tests can reach it.

my-service/
  Cargo.toml          # [dependencies], [profile.release], optional [workspace]
  src/
    main.rs           # entrypoint: parse config, build state, axum::serve — wiring only
    lib.rs            # pub mod error; pub mod app; pub mod users; — the testable surface
    error.rs          # the thiserror enum + IntoResponse
    users/
      mod.rs          # handlers + the domain model
      repo.rs         # sqlx queries
  tests/
    users_api.rs      # integration tests that spin up the Router

A larger system becomes a Cargo workspace ([workspace] members = [...]) with one crate per bounded context. Gate optional deps behind [features]. The lib.rs carries #![forbid(unsafe_code)].

Testing (essentials)

#[test] for sync, #[tokio::test] for async; integration tests under tests/ exercise the real Router; doctests keep examples honest.

#[tokio::test]
async fn get_user_404_when_missing() {
    let app = build_router(test_state());            // the same Router main builds
    let res = app
        .oneshot(Request::get("/users/999").body(Body::empty()).unwrap())
        .await
        .unwrap();
    assert_eq!(res.status(), StatusCode::NOT_FOUND);
}

Use cargo nextest run for faster, cleaner parallel runs; cargo test --doc for doctests. Trait-based fakes (a UserRepo trait the handler depends on, a fake impl in tests) keep the DB out of unit tests. Integration matrices, insta snapshots, and the full tests/ HTTP setup -> references/testing.md.

Security (embedded)

Parametrize SQL, forbid unsafe, audit dependencies, read secrets from the environment:

// Good: bound parameters; sqlx checks the query at compile time against the DB schema.
sqlx::query_as!(User, "SELECT id, name FROM users WHERE id = $1", id).fetch_one(&pool).await?;

// Bad: format! into SQL is injection, full stop.
// sqlx::query(&format!("SELECT * FROM users WHERE id = {id}")).fetch_one(&pool).await?;

#![forbid(unsafe_code)] at the crate root; run cargo audit (RustSec advisories) and cargo deny (license

  • ban + advisory policy) in CI; never .unwrap() on untrusted input — a malicious request becomes a panic. Read secrets from env or a secret manager, never hardcode or log them. Deeper authz / threat modeling -> secure-coding. Pure SQL schema/index/plan tuning -> postgresdb; this skill covers only the Rust-side sqlx query.

Production

Structured logs and a lean release binary:

// JSON tracing subscriber, level from RUST_LOG; do this once in main before serving.
tracing_subscriber::fmt().json().with_env_filter(tracing_subscriber::EnvFilter::from_default_env()).init();
[profile.release]
lto = true              # link-time optimization: smaller, faster binary
codegen-units = 1       # better optimization at the cost of compile time
panic = "abort"         # no unwinding in prod; smaller binary, fail fast
strip = true            # strip symbols

Expose /healthz (static 200 liveness) and /readyz (pings the DB pool, 503 on failure). Docker: multi-stage build, cargo build --release, copy the binary onto a distroless/slim base. Full Containerfile + CI -> deployment.

Anti-patterns

Anti-pattern Reality / Do instead
.clone() to make the borrow checker happy It hides the real ownership question; borrow, or restructure who owns what.
.unwrap() / .expect() off the test path A panic on the request path is a 500 or a crashed worker; use ? + a typed error.
Matching an error by its message string Messages are prose and they change; match the enum variant.
Box<dyn Error> everywhere because it is simpler Nothing can branch on the failure; use a thiserror enum the caller can match.
#[async_trait] on every async trait Edition 2024 has native async fn in traits; drop the macro for most cases.
block_on inside an async fn Nesting a runtime panics or deadlocks; restructure to .await.
A bare tokio::spawn per loop iteration Unbounded fan-out exhausts the runtime; bound it with JoinSet/semaphore.
Arc<Mutex<T>> for everything shared If data flows one way it is a channel; reach for mpsc first.
unsafe to get past the borrow checker unsafe turns a compile error into UB; the checker was right — restructure.
Skipping clippy as "just style" clippy catches correctness (.unwrap() on Option, await-holds-lock); gate on -D warnings.

Gates & commands

Task Command
Format check cargo fmt --all -- --check
Lint (gate) cargo clippy --all-targets -- -D warnings
Test cargo test / cargo nextest run
Doctests cargo test --doc
Audit deps cargo audit / cargo deny check
Local gate ./scripts/verify.sh (run in your crate root)

Format and lint are build gates, not suggestions.

Project grounding (02-DOCS)

In a project with a 02-DOCS/ layer (the harness wiki), the service decisions live in 02-DOCS/wiki/stack/rust.md, indexed from 02-DOCS/wiki/index.md. Read it first and stay consistent; if it is missing or stale, write the project's real choices there — crate/workspace layout, runtime (tokio), HTTP framework (axum 0.8), error strategy (thiserror enum + IntoResponse mapping), DB layer (sqlx + pool), tracing and concurrency defaults — bump its Updated date, and index it. No 02-DOCS/? Skip silently. Conventions are recorded, not gated — never block the task on this.

go is the structural twin: same write/review/test/ship service shape, GC + goroutines + multi-return errors instead of ownership + futures + Result. A desktop shell around a webview is tauri, not this.

Use it

Copy one of these into your project. Installing also returns the manifest and these snippets.

yaml
targets:
  - https://api.opensmartroute.ai/api/v1/registry/ericrisco-rsc-harness-rust/manifest   # or paste the manifest below

Manifest

An Open Capability Manifest: the router reads it to know what this does, what it costs and when to pick it.

ericrisco-rsc-harness-rust.ocm.jsonjson
{
  "ocm": "1",
  "id": "ericrisco-rsc-harness-rust",
  "kind": "skill",
  "name": "rust",
  "description": "Use when writing, reviewing, testing, or shipping Rust — ownership and the borrow checker (move/borrow/clone, Arc/RefCell, lifetimes), errors with Result/`?`/thiserror/anyhow, async on tokio, axum 0.8 services, cargo test, and sqlx + cargo-audit hardening. NOT the same service in Go (that is `go`), NOT a desktop webview shell (that is `tauri`).",
  "publisher": "ericrisco",
  "version": "1.0.0",
  "capabilities": {
    "domains": [
      "coding"
    ],
    "tags": [
      "skill-md",
      "rust",
      "tokio",
      "axum",
      "async",
      "backend",
      "service",
      "github"
    ],
    "languages": [
      "en"
    ]
  },
  "quality_prior": 0.6,
  "examples": [
    "Use when writing, reviewing, testing, or shipping Rust — ownership and the borrow checker (move/borrow/clone, Arc/RefCell, lifetimes), errors with Result/`?`/thiserror/anyhow, async on tokio, axum 0.8 services, cargo test, and sqlx + cargo-audit hardening. NOT the same service in Go (that is `go`), NOT a desktop webview shell (that is `tauri`)."
  ],
  "primary": false,
  "metadata": {
    "source": {
      "provider": "github",
      "repository": "https://github.com/ericrisco/rsc-harness",
      "path": "skills/rust/SKILL.md",
      "ref": "8cc4716ea549275ade1590ad270da01bdf837ab5",
      "url": "https://github.com/ericrisco/rsc-harness/blob/8cc4716ea549275ade1590ad270da01bdf837ab5/skills/rust/SKILL.md",
      "key": "ericrisco/rsc-harness/skills/rust/SKILL.md"
    }
  },
  "instructions": "# Idiomatic Rust services\n\nWrite, review, test, and ship idiomatic async Rust services with the ownership model working *for*\nyou, not against you.\n\nTargets **Rust 1.85+ / edition 2024** as the floor: native `async fn`\nin traits (no reflexive `#[async_trait]`), **tokio 1.x** as the runtime, **axum 0.8** for the HTTP\nsurface (`{id}` path-capture syntax, async-trait-free extractors), **thiserror 2** for library error\nenums and **anyhow** at the application edge, **sqlx** for compile-time-checked SQL, and **tracing**\nfor structured observability.\n\nThe thing an agent gets wrong in Rust is almost n",
  "cost": {
    "context_tokens": 3520
  }
}

Fetch it by URL: GET /api/v1/registry/ericrisco-rsc-harness-rust/manifest?version=1.0.0

Reviews

Star ratings from people who tried it. One review per account; edit yours any time.

No reviews yet. Install it, try it, and be the first to rate it.