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

Writing a client

#[openapi_trait::client(...)] generates the same models as the server macro, plus a transport-agnostic {Mod}Client trait:

#[openapi_trait::client("examples/pets.openapi.yaml")]
pub mod pets {}

You can implement that trait yourself, or use the generated reqwest implementation.

Implementing the trait yourself

Every method takes the operation’s request plus an Option<openapi_trait::RequestOptions> and returns the operation’s response enum:

/// An in-memory stand-in for the real API.
struct FakePets;

impl pets::PetsClient for FakePets {
    type Error = std::convert::Infallible;

    async fn list_pets(
        &self,
        _req: pets::list_pets::Request,
        _options: Option<openapi_trait::RequestOptions>,
    ) -> Result<pets::list_pets::Response, Self::Error> {
        Ok(pets::list_pets::Response::Status200(vec![]))
    }

    async fn create_pet(
        &self,
        req: pets::create_pet::Request,
        _options: Option<openapi_trait::RequestOptions>,
    ) -> Result<pets::create_pet::Response, Self::Error> {
        Ok(pets::create_pet::Response::Status201(pets::Pet {
            id: 1,
            name: req.body.name,
            status: req.body.status,
        }))
    }

    async fn get_pet(
        &self,
        _req: pets::get_pet::Request,
        _options: Option<openapi_trait::RequestOptions>,
    ) -> Result<pets::get_pet::Response, Self::Error> {
        Ok(pets::get_pet::Response::Status404)
    }
}

Code that depends on the trait doesn’t care which implementation it gets, so fakes like this one make good test doubles:

/// Code written against the trait works with any implementation.
async fn pet_name<C: pets::PetsClient>(client: &C, pet_id: i64) -> Option<String> {
    match client
        .get_pet(pets::get_pet::Request { pet_id }, None)
        .await
    {
        Ok(pets::get_pet::Response::Status200(pet)) => Some(pet.name),
        _ => None,
    }
}

Unlike the server trait, client methods have no default bodies. You implement every operation.

The reqwest client

With the default reqwest-client feature, the macro also generates a blanket {Mod}Client implementation. Any type can use it by deriving openapi_trait::ReqwestClient:

/// Holds everything the generated client implementation needs.
#[derive(Clone, openapi_trait::ReqwestClient)]
struct PetsHttpClient {
    /// The HTTP client used for every request.
    #[openapi_trait(client)]
    http: openapi_trait::reqwest::Client,
    /// Base URL that operation paths are appended to.
    #[openapi_trait(base_url)]
    base_url: String,
    /// Credentials for the spec's security schemes.
    #[openapi_trait(auth)]
    auth: pets::PetsAuthState,
}
Field attributeField typeRequired
#[openapi_trait(client)]openapi_trait::reqwest::Clientyes
#[openapi_trait(base_url)]Stringyes
#[openapi_trait(auth)]{Mod}AuthStatewhen the spec declares security schemes

A field named auth is picked up without the attribute.

Then call operations through the trait:

    use pets::PetsClient as _;
    use pets::PetsClientAuth as _;

    let client = PetsHttpClient {
        http: openapi_trait::reqwest::Client::new(),
        base_url: "http://127.0.0.1:3000".into(),
        auth: pets::PetsAuthState::default(),
    }
    .with_bearer_auth("secret");

    let created = client
        .create_pet(
            pets::create_pet::Request {
                body: pets::NewPet {
                    name: "doggie".into(),
                    status: Some(pets::PetStatus::Available),
                },
            },
            None,
        )
        .await?;
    println!("created: {created:?}");

    let all = client
        .list_pets(pets::list_pets::Request { limit: Some(10) }, None)
        .await?;
    println!("listed: {all:?}");

The generated implementation returns Reqwest{Mod}ClientError, an enum covering transport errors (Transport), response bodies that don’t parse as the declared type (Decode, with the serde_json error as its source), missing credentials, malformed response headers, and status codes the spec doesn’t declare (UnexpectedStatus, which includes the body). When the operation has a default response, an undeclared status isn’t an error: it’s decoded into Response::Default { status, body, … }.

The enum is #[non_exhaustive], so variants can be added without a breaking change. That only matters for crates that depend on the one containing the generated module: inside it, a match can still list every variant.

Per-request headers and auth

Pass Some(RequestOptions) to add headers or override auth for a single call without changing the client:

    let pet = client
        .get_pet(
            pets::get_pet::Request { pet_id: 1 },
            Some(
                openapi_trait::RequestOptions::new()
                    .bearer_auth("another-token")
                    .header("X-Request-Id", "abc-789"),
            ),
        )
        .await?;

RequestOptions provides header(name, value), bearer_auth(token) and basic_auth(username, Option<password>). Options are applied after the operation’s declared headers and credentials:

  • bearer_auth and basic_auth replace any Authorization header set by the client’s security scheme. They also satisfy the credential check, so a call can succeed even if the client has no stored credentials.
  • header appends to the request.

Binary downloads

Operations that return a binary body give you an openapi_trait::ByteStream, a futures_core::Stream of bytes::Bytes chunks. Read it incrementally instead of buffering the whole response.

Without reqwest

To generate only the transport-agnostic trait and drop the reqwest dependency:

[dependencies]
openapi-trait = { version = "1", default-features = false }