Quick Start: A Runnable Client in One Sitting
This chapter goes from an empty project to a compiling, querying client in one
file. It is deliberately the shortest path — one endpoint against the
framework’s BaseConfig, no config or error type of your own — so the whole
shape is visible at once. Scaffolding a Client Crate
onward builds the same thing properly; here every corner that can be cut is
cut.
The endpoint below is the same first request the capi_rs crate
documentation and README teach, with a real transport in the client seat.
Prerequisites
- Rust edition 2021, MSRV 1.88.
- No serde knowledge: response types describe themselves through the framework’s own wire derives.
Step 1: Add dependencies
cargo new parcel_quickstart && cd parcel_quickstart
[dependencies]
capi_rs = { version = "0.5", features = ["async"] }
capi_wire_model = { version = "0.5", features = ["derive"] }
capic_reqwest = { version = "0.5", features = ["async"] }
capiw_serde_json = "0.5"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
Four framework crates. capi_rs is the facade: it re-exports the Capi
framework workspace and nothing else, so the wire model is a dependency
of its own — the derives it exports generate code that names
::capi_wire_model directly. capic_reqwest is the transport, and
capiw_serde_json the JSON codec. std is a default feature everywhere and
needs no mention; async is the one feature this build turns on, and it is
turned on for both crates that carry the axis, the facade and the transport.
The compilation target — not a feature — picks the platform, which
Features and Targets explains.
Step 2: Write the client
One file. It fetches a shipment from an imaginary parcel API.
use capi_rs::core::context::ApiContext;
use capi_rs::prelude::*;
use capi_wire_model::{WireDecode, WireModel};
// 1. The endpoint is a plain struct. `#[capi]` gives it a constructor
// and setters; `ApiEndpoint` turns its fields into the request plan.
#[capi]
#[derive(Debug, Clone, ApiEndpoint)]
#[endpoint(path_template = "/v1/shipments/{shipment_id}")]
struct GetShipment {
#[endpoint(location = "path")]
pub shipment_id: String,
}
// 2. The response type describes itself to the wire model — no serde.
#[derive(Debug, WireModel, WireDecode)]
struct Shipment {
id: String,
status: String,
}
// 3. The Endpoint impl names the method, the URL, and the decoder.
impl Endpoint for GetShipment {
type ApiConfig = BaseConfig;
type Output = Shipment;
type Error = CodecError;
type Decoder = BodyDecoder;
type Requires = Http;
fn method(&self) -> Method {
Method::GET
}
fn url(&self, config: &BaseConfig, _context: &ApiContext) -> impl ToUrl {
config.base_url().path(self.path_string())
}
fn auth(&self) -> Auth {
Auth::NONE
}
}
// 4. `BodyDecoder` hands the response body to this one function.
impl DecodeBody for GetShipment {
fn decode_body(
body: Vec<u8>,
_head: ResponseHead,
_config: &BaseConfig,
_context: ApiContext,
) -> Result<Shipment, ResponseError<CodecError>> {
capiw_serde_json::codec().decode_from_slice(&body).map_err(Into::into)
}
}
// 5. Pair a config with a client, then query.
#[tokio::main]
async fn main() -> Result<(), QueryError> {
let config = BaseConfig::new(UrlBuilder::https("api.parcel.example"));
let api = BaseInterface::new(config, capic_reqwest::ReqwestClient::default());
let shipment = api.query(GetShipment::new("shp_123")).await?;
println!("{}: {}", shipment.id, shipment.status);
Ok(())
}
What just happened
Each numbered piece maps onto the pipeline:
- The endpoint (
GetShipment) is the letter: it states the method and URL and nothing about how the request travels.#[capi]generatedGetShipment::new(shipment_id)— the field is neitherOption,bool, norVec, so it is a constructor parameter — andApiEndpointgeneratedpath_string(), which fills the template from the field markedlocation = "path".type Requires = Httpdeclares that the endpoint needs plain HTTP; a WebSocket endpoint would sayWebSocket, and a client that cannot perform the exchange is refused at the query call site.headers,query_string,body, and the rest are provided methods this endpoint did not need to override. auth()is overridden on purpose. The default isAuth::DEFAULT, a declaration that the request needs the config’s default credential, andBaseConfiginstalls no credential store — so the seal that places credentials would refuse the request before it is sent.Auth::NONEsays the endpoint carries no credential. A real client’s config holds a store, and its endpoints leave the default in place; that is the authentication part of the book.- The decoder turns the response body back into a
Shipment.BodyDecoderbuffers the body and hands it todecode_bodywhatever the status; here a malformed body or a404both surface as aDecodingerror, because nothing has told the framework what the service’s error bodies look like. A real client checks the status first and decodes an error envelope on the failure path — the status ladder — and the framework’sResponseErrorcarries the result either way. decode_from_sliceis the codec call. Namingcapiw_serde_json::codec()inline keeps the example to one file; a real client stores aCodec<Json>on its own config and reads it back throughconfig.json_lib(), so the JSON engine stays swappable — the config chapter.BaseInterface::new(config, client)is where the two swappable choices meet: the config (base URL — and normally the codecs and the credential store) and the transport. SwapReqwestClientforcapic_ureq::UreqClientand the same endpoint runs blocking; the endpoint code does not change.api.query(...)is the one funnel. It returnsResult<Shipment, QueryError>;?bubbles up any failure the framework owns — transport, decode, an unexpected status — as aQueryError.
Real endpoints add one derive to step 1: #[derive(WireModel, ApiEndpoint)].
WireModel is what lets #[wire(...)] attributes shape a field’s wire
name and presence, and what gives the endpoint its redacted twin for
fixtures and logs. The endpoint contract shows
the full form.
What’s next
| Chapter | What you’ll learn |
|---|---|
| The Five Ideas and the Request Lifecycle | The mental model the rest of the book assumes |
| The Wire Model | What Codec<Json> and the wire derives are |
| Scaffolding a Client Crate | Turning this one file into a real crate with its own config |
| Consuming and Patching a Client | Using — and fixing — a client someone else shipped |