Crate http_typed

source ·
Expand description

HTTP client supporting custom request and response types. Pass any type into a send function or method, and it will return a result of your desired response type. send handles request serialization, http messaging, and response deserialization.

To keep this crate simple, it is is oriented towards a specific but very common pattern. If your use case meets the following conditions, this crate will work for you:

  1. request-response communication
  2. async rust functions
  3. communicate over http (uses reqwest under the hood)
  4. http body is serialized as json
  5. status codes outside the 200 range are considered errors
  6. request and response types must be serializable and deserializable using serde
  7. the path and HTTP method can be determined from the concrete rust type used for the request



To use this library, your request and response types must implement serde::Serialize and serde::Deserialize, respectively.

To take full advantage of all library features, you can implement Request for each of your request types, instantiate a Client, and then you can simply invoke Client::send to send requests.

let client = Client::new("");
let response = client.send(MyRequest::new()).await?;


If you don’t want to implement Request or create a Client, the most manual and basic way to use this library is by using send_custom.

let my_response: MyResponse = send_custom(


One downside of the send_custom (and send) function is that it instantiates a client for every request, which is expensive. To improve performance, you can use the Client::send_custom (and Client::send) method instead to re-use an existing client for every request.

let client = Client::default();
let my_response: MyResponse = client.send_custom(


You may also prefer not to specify metadata about the request every time you send a request, since these things will likely be the same for every request of this type. Describe the request metadata in the type system by implementing the Request trait.

pub trait Request {
    type Response;
    fn method(&self) -> HttpMethod;
    fn path(&self) -> String;

This increases design flexibility and reduces boilerplate. See the API Client Design section below for an explanation.

If you do not control the crate with the request and response structs, you can implement any traits for them using the newtype pattern, or with a reusable generic wrapper struct.

After implementing this trait, you can use the send function and method, which requires the base url to be included, instead of the full url. All other information about how to send the request and response is implied by the type of the input. This still creates a client on every request, so the performance is not optimal if you are sending multiple requests.

let my_response = send("", MyRequest::new()).await?;
// The type of my_response is determined by the trait's associated type.
// It does not need to be inferrable from the calling context.
return my_response.some_field

If you want to send multiple requests, or if you don’t want to include the base url when calling send, instantiate a Client:

let client = Client::new("");
let my_response = client.send(MyRequest::new()).await?;

Request groups

You can also define request groups. This defines a client type that is explicit about exactly which requests it can handle. The code will not compile if you try to send a request with the wrong client.

request_group!(MyApi { MyRequest1, MyRequest2 });
let my_client = Client::<MyApi>::new("");
let my_response1 = my_client.send(MyRequest1::new()).await?; // works
let other_response = my_client.send(OtherRequest::new()).await?; // does not compile


If you want to restrict the request group, but still want to include the url for every call to send, MyClient has a send_to method that can be used with the default client to specify the url at the call-site.

let my_client = Client::<MyApi>::default();
let my_response2 = my_client.send_to("", MyRequest2::new()).await?; // works
let other_response = my_client.send_to("", OtherRequest::new()).await?; // does not compile

The send_to method can also be used to insert a string after the base_url and before the Request path.

let my_client = Client::new("");
let my_response = my_client.send_to("/api/v2", MyRequest::new()).await?;

Cargo Features

Typically, the default features should be fine:

http-typed = "0.3"

The default features include the full Client implementation, and depend on system tls libraries.

All features:

  • default = [“client”, “native-tls”]
  • client: Includes the Client implementation described above and depends on reqwest.
  • native-tls: Depend on dynamically linked system tls libraries.
  • rustls-tls: Statically link all tls dependencies with webpki, no tls is required in the system.

No system tls? Use rustls

To statically link the tls dependencies, use this:

http-typed = { version = "0.3", default-features = false, features = ["client", "rustls-tls"] }

No Client

If you’d like to exclude the Client implementation and all of its dependencies on reqwest and tls libraries, use this:

http-typed = { version = "0.3", default-features = false }

This allows you, as a server developer, to exclude unnecessary dependencies from your server. For example, you may have an API crate with all the request and response structs, which you both import in the server and also make available to clients. You can feature gate the client in your API library:

# api library's Cargo.toml

default = ["client"]
client = ["http-typed/client"]

…and then you can disable it in the server’s Cargo.toml. Something like this:

# server binary's Cargo.toml

my-api-client = { path = "../client", default-features = false }

A similar pattern can be used to give clients the option between native-tls and rustls-tls.

API Client Design

Normally, a you might implement a custom client struct to connect to an API, including a custom method for every request. In doing so, you’ve forced all dependents of the API to make a choice between two options:

  1. use the specific custom client struct that was already implemented, accepting any issues with it.
  2. implement a custom client from scratch, re-writing and maintaining all the details about each request, including what http method to use, what path to use, how to serialize/deserialized the message, etc.

Instead, you can describe the metadata through trait definitions for ultimate flexibility, without locking dependents into a client implementation or needing to implement any custom clients structs. Dependents of the API now have better options:

  1. use the Client struct provided by http-typed, accepting any issues with it. This is easier for you to support because you don’t need to worry about implementation details of sending requests in general. You can just export or alias the Client struct.
  2. implement a custom client that can generically handle types implementing Request by using the data returned by their methods. This is easier for dependents because they don’t need to write any request-specific code. The Request trait exposes that information without locking them into a client implementation. Only a single generic request handler is sufficient.

Other use cases

If your use case does not meet some of the conditions 2-7 described in the introduction, you’ll find my other crate useful, which individually generalizes each of those, allowing any of them to be individually customized with minimal boilerplate. It is currently a work in progress, but almost complete. This crate and that crate will be source-code-compatible, meaning the other crate can be used as a drop-in replacement of this one without changing any code, just with more customization available.


  • Define a request group to constrain which requests can be used with a client.


  • The default group. All requests are in this group.
  • A client to delegate to the send function that provides the ability to optionally specify:



  • Indicates that a request is part of a request group. If you use the request_group macro to define the group, it will handle the implementation of this trait automatically.
  • This allows the send_custom methods to accept objects that do not implement Request. SimpleBody is a more minimal requirement that you get automatically if you implement request, but you can also implement this by itself without implementing Request, to keep things simple with usages of send_custom.


  • Convenience function to create a client and send a request using minimal boilerplate. Creating a client is expensive, so you should not use this function if you plan on sending multiple requests.
  • Convenience function to create a client and send a request using minimal boilerplate. Creating a client is expensive, so you should not use this function if you plan on sending multiple requests.