Implementing REST client with Rust: an introduction to traits and generics
Tags: rust, rest, refactoring
Published: 2017-12-27 09:21:00

---
title: "Implementing REST client with Rust: an introduction to traits and generics"
published: true
description: "Practical example how Rust REST client library interface was refactored using traits and generics."
tags: rust, rest, refactoring
canonical_url: https://pagefault.blog/2017/12/23/implementing-rest-client-with-rust/
---

After reading the Rust book and [writing about my initial experiences](https://pagefault.blog/2017/09/14/my-experiences-learning-rust/), I wanted a small practical project to familiarize myself with the language. So, I decided to write a REST client library. This post shows, step-by-step, how my initial design was refactored to be more robust and easy-to-use. Traits and generics are key concepts in these refactorings. The intent is not to cover the internals of the library in detail and also the basic syntax of traits and generics is not covered here. The Rust Book [chapter 10](https://doc.rust-lang.org/book/second-edition/ch10-00-generics.html) is an excellent resource for the latter.

## Restson

The library I implemented is called Restson and it is a client library for JSON encoded REST APIs. The main idea is to provide easy-to-use interface for API requests and also handle URL parsing as well as serialization and deserialization from Rust structs automatically. The library is implemented using Hyper, which is the de-factor HTTP library for Rust. 

The Restson library is available in [GitHub](https://github.com/spietika/restson-rust) and in [crates.io](https://crates.io/crates/restson).

## Design iterations

When I started writing the library I was new to Rust (I still am) and I had also never used Hyper before. So I did not end up with elegant design right away. Though, usually you don't end up with the final design right away even if you are familiar with the language. At least for me it usually takes some iterations and refactoring to improve and refine the initial ideas and structures.

Lets look at how the GET interface signature looked initially:
```rust
pub fn get(&mut self, url: String) -> Result<String, Error>
```
So the function took the REST endpoint URL as a parameter, performed the GET request with Hyper library and then returned the request body as a string. From functionality point-of-view the function above is not _wrong_ because it does get the job done. However, it is not ideal for the library user and there are also multiple disadvantages with this design.

* The whole URL needs to be provided every time the interface is used:
    * URL literals scattered in the code
    * Changing the base URL requires modifying all the API calls
* Client caller needs to handle URL parsing (which adds boilerplate and is error prone)
* Client caller needs to manually deserialize the returned JSON in GET and serialize data to JSON in POST.

Lets look at how these disadvantages can be fixed by improving the library interface.

### Automatic (de)serialization

Lets put the URL parsing aside for now, and look at the return value of the function first. For simple use cases it might be ok to just manipulate the JSON directly, but it is often better to deserialize it to struct. This allows the compiler to do proper type checks and also reduces dependencies to the exact JSON format. For instance, if some field name changes, only the (de)serialization code needs to be updated instead of all the places where the data is used.

To be able to return deserialized data, the `get`-function needs to know the type in which the JSON is deserialized to. However, these types are defined by the library user and cannot be fixed in the library code. This can be solved by making `get` a generic function:

```rust
pub fn get<T>(&mut self, url: String) -> Result<T, Error> where
    T: serde::de::DeserializeOwned
```
Generic type `T` is introduced and instead of returning `Result<String, Error>` the function now returns `Result<T, Error>`. Now the function can return different user defined types that are deserialized from the JSON returned by the server. In Rust, type inference also makes generics convenient to use:

```rust
let data: MyType = client.get("url").unwrap();
```

In the code above, the compiler is able to figure out that generic type `T` in `get` must be `MyType` because the return value is assigned to variable of that type (return value of `unwrap` to be exact). The generic type could also be explicitly annotated but it is unnecessary here. The call above is equivalent with the example below.

```rust
let data = client.get::<MyType>("url").unwrap();
```

You might be wondering what is the `where T: serde::de::DeserializeOwned` part for. Well, the generic type `T`cannot be any type. The `get`-function can only accept types that implement deserialization interface. In Restson library, the deserialization is implemented using Serde library, so type `T` needs to implement `DeserializeOwned`trait. This way the implementation of `get` can call `serde_json::from_str` function for that type.

The `where` clause sets a trait bound which makes sure that if `T` does not implement `DeserializeOwned` and thus does not have `serde_json::from_str` function available, the build won't pass. For the library user, implementing this trait is easy thanks to macros provided by Serde.

```rust
#[derive(Deserialize)]
struct MyType {
    field1: String,
    field2: String,
}
```

By using a generic type for the return value, the library is now able to automatically deserialize the data to user defined types. Relatively minor change on the library that makes the interface more convenient to use.

### URL handling

First obvious improvement to the URL handling is to give the base URL when the client instance is created. Then the individual API functions like `get` would only take the REST path as a parameter.

This is already better, but still error prone. It would still be easy to use wrong type of return value with an API path. This would mean that deserialization would always fail, and the error would not be caught during compilation. Furthermore, there would still be path string literals scattered in the code.

One approach to solve this would be to associate the API path with the type that is used with the library. That is, each type that is used in REST requests would be able to return the corresponding API path. This is accomplished by defining a trait for the REST path.

```rust
pub trait RestPath {
    fn get_path() -> String;
}
```

```rust
pub fn get<T>(&mut self) -> Result<T, Error> where
    T: serde::de::DeserializeOwned + RestPath
```

Now, each type that is used as the return type `T`, must implement both `DeserializedOwned` and `RestPath` traits. This is also enforced by the compiler, and if the traits have not been implemented the build will fail. The URL parameter can be removed from `get` altogether because the implementation can call `T::get_path()` to get the correct path and the base URL is provided when the client is instantiated. The trait implementation simply needs to return the correct path string:

```rust
impl RestPath for MyType {
    fn get_path() -> String { String::from("apipath") }
}
```

Now the API path is only defined once and the interface can be used without even knowing what the actual path string is:

```rust
let data: MyType = client.get().unwrap();
```

There is still one major weakness in this design. For instance, a path like `api/devices/<ID>/status` would be difficult to implement because it contains a parameter that should be easily changeable. However, the current `get_path` does not take any parameters.

### Parametrized paths

To be able to return parametrized API paths, the `get_path` functions should take a parameter that is used to format the path string. For instance:

```rust
impl RestPath for MyType {
    fn get_path(id: u32) -> String { format!("api/devices/{}/status", id) }
}
```

This parameter would also need to be forwarded from the interface functions such as `get`. The problem is, however, that this is library code and cannot know what types the library user would like to use as path parameters. One way to dodge this problem would be to pass, for instance, a vector of strings. Although this would work, it is not optimal. For one, it would require unnecessary string conversion code for the library user and also make error checking in the trait implementation harder. Better alternative is to make `RestPath` trait generic.

```rust
pub trait RestPath<U> {
    fn get_path(par: U) -> Result<String, Error>;
}
```

Now the library user can use whatever parameter type to implement the trait. The return value is also changed to `Result<String, Error>` so that the trait implementation can indicate errors with the provided parameters. These changes also require changes to `get` interface to pass the parameters.

```rust
pub fn get<U, T>(&mut self, param: U) -> Result<T, Error> where
    T: serde::de::DeserializeOwned + RestPath<U>
```

Another generic parameter `U` is introduced to the interface which is used in the trait bound `RestPath<U>`, and also taken in as `param`. When the library implementation calls `get_path` function, the parameter is passed to it `T::get_path(params)`. If a `RestPath` implementation for type `T` with correct parameter type is not found, the compiler produces an error. The interface now looks pretty complex but it is actually pretty simple to use.

First the `RestPath` is implemented for user type:

```rust
impl RestPath<u32> for MyType {
    fn get_path(id: u32) -> Result<String,Error> { 
        Ok(format!("api/devices/{}/status", id)) 
    }
}
```

Then the path parameter is simply given to the interface function which passes it to `get_path`.

```rust
// calls api/devices/1234/status and deserializes
// returned JSON to data variable
let data: MyType = client.get(1234).unwrap();
```

The `RestPath` trait can also be implemented multiple times for the same type with different generic parameters. It is also possible to provide multiple path parameters by using, for instance, a tuple.

## Conclusions

These refactorings improved the initial design which, although working, was not very convenient for the library user. It was also prone to errors that could not easily be detected during compilation. By using a combination of generics and traits the interface was kept simple to use but still a lot of common functionality (such as serialization) was encapsulated inside the library. User types were also allowed with the library which enables more error checking by the compiler (compared to passing things as, for instance, strings).

All in all the traits and generics are powerful tools to have especially when writing library, utility or other reusable code.