I'm trying to create an ergonomic API client for an API that has a large number of endpoints.
I looked through several articles and API clients on crates.io and there were several ways I thought of organising the code, some were too complex for this use case and I ended up with issues like:
- long method names if all methods were implemented on a single client and all in the same module/file
- needing to pass the client to every request if breaking into separate modules
- unfriendly user syntax like
(Endpoint as UserEndpoint).get_by_id("12345")
if organising with traits. - ...
I jotted down this notation, where a client is created with the base server and reqwest details, and each endpoint is a member of the client struct with it's own methods, so something like this..
let client = Client::new("server.org", "user_key");
let user = client.users.get_by_id("12345");
The following code works but I can't shake the feeling that my use of the reqwest::Client is weird, particularly because the reqwest::Client has an internal Arc and can be cloned directly. In the code, the main Client should have the single copy of the reqwest::Client and each endpoint has a reference to it. Each method on the endpoint clones it's own reference when calling a method.
Is this a good way to implement this? Can I use a reqwest::Client, clone it into the endpoints, and clone the clone on each method? Do I even need to clone the client again on the endpoint method level? Does anyone see any problems with this implementation or have any recommendations?
pub struct Client {
api_base: url::Url,
client: Arc<reqwest::Client>,
pub users: UsersEndpoint,
}
impl Client {
pub fn new(domain: &str, key: &str) -> Result<Client> {
let api_base = Url::parse(format!("https://{domain}/api/").as_str())?;
let mut headers = HeaderMap::new();
headers.insert("x-key-header", HeaderValue::from_str(key)?);
// set default client operation
let client = Arc::new(reqwest::Client::builder().default_headers(headers).build()?);
// register endpoints
let users = UsersEndpoint:new(&api_base, client.clone())?;
Ok(Client { api_base, client, users, })
}
}
pub struct UsersEndpoint {
endpoint: url::Url,
client: Arc<reqwest::Client>,
}
impl UsersEndpoint {
pub fn new(client: Arc<reqwest::Client>, api_base: &Url) -> Result<Self> {
Ok(ApplicantEndpoint { endpoint: api_base.join("users/")?, client, })
}
pub async fn get_by_id(&self, id: &str) -> Result<String> {
let client = self.client.clone();
let endpoint = self.endpoint.join(id)?;
Ok(client.get(endpoint).send().await?.text().await?)
}
}