Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Capi vs the Alternatives

Before committing to a framework it’s fair to ask what you’d do instead, and when the plainer option is the right one. This chapter positions Capi against the two things Rust developers actually reach for when they need to talk to an HTTP API.

Option 1: hand-roll a reqwest client

The default move is a struct wrapping reqwest::Client with one method per endpoint:

impl GitHubClient {
    pub async fn get_repo(&self, owner: &str, repo: &str) -> Result<Repo, MyError> {
        let url = format!("https://api.github.com/repos/{owner}/{repo}");
        let resp = self.http.get(&url).bearer_auth(&self.token).send().await?;
        if !resp.status().is_success() {
            return Err(/* map status + error body, somehow */);
        }
        Ok(resp.json().await?)
    }
    // ... one of these per endpoint, each re-deriving the same plumbing ...
}

This is fine for three endpoints. It stops being fine because every such client re-implements the same cross-cutting machinery from scratch — status-to-error mapping, retry, pagination, rate limiting, streaming — and hardcodes three choices into its very type:

  • reqwest, so the crate can’t serve a ureq (blocking) or wasm or embedded user without a parallel implementation.
  • tokio, so every consumer inherits an async runtime whether they want one or not.
  • serde_json, so an API that also speaks XML or form-encoding becomes a special case bolted onto the side.

Those are the application author’s decisions, baked in by the library author. A consumer who disagrees has to fork.

Option 2: a builder-style request crate

The other common shape is a crate that gives you an ergonomic request builder — you assemble method, URL, headers, and body fluently and call .send(). This removes some boilerplate, but the format, the client, and the runtime are still fixed by the builder, and the response side (typed errors, paged iterators, streaming, reconnection) is left to you. You’ve made the request nicer to write; you haven’t decoupled anything.

How Capi inverts it

Capi keeps the three hardcoded choices as swappable seams and moves the cross-cutting machinery into the framework. The endpoint becomes a value that describes the call and nothing else; the transport, the codec, and the runtime are chosen by the application at the interface. The decoupling thesis is the full argument; here is the practical comparison:

Hand-rolled reqwestBuilder-style crateCapi
Sync and async from one definition
wasm / embedded targetsrarely
Pluggable HTTP client
Pluggable, type-checked wire codec
Typed domain errors + framework-owned generic failuresyou write ityou write it
Streaming / pagination / WebSocket first-classyou write ityou write it
Downstream can patch an endpoint without forking
A credential store with seated renewal and request signers, shared by every endpointyou write ityou write it
A client that cannot perform an exchange is refused at compile time
Replay tests from recorded, redacted fixturesyou write ityou write it

The type-checked codec row is the one most alternatives can’t reach at all: because a codec is a Codec<Format> handle carrying a compile-time format tag, a boundary that requires JSON rejects an XML codec before the program runs — swappability without giving up safety.

When it’s overkill, and when it pays off

Reach for a hand-rolled reqwest client when the client is small, private, and will only ever run one way — a handful of endpoints, one format, async-on-tokio, no plans to ship it to anyone with different constraints. The abstraction isn’t worth it there, and this book will happily tell you so.

Reach for Capi when any of these is true:

  • You’re publishing a client others will consume, and can’t predict their runtime, client, or format.
  • You need one codebase to serve sync, async, and wasm (or embedded) targets.
  • The API has real response complexity — pagination, SSE/streaming, WebSocket, reconnection, deferred/long-poll — that you’d otherwise re-implement by hand.
  • You want consumers to patch a broken or missing endpoint without forking your crate.

The rest of this part gets you to a running client; if you’re weighing adoption, the design-rationale capstone lays out exactly which parts are hard requirements and which are conveniences you can opt out of.