REST with Rust

REST with Rust

Calling Rest APIs with Rust

Calling REST APIs with rust may seem like a daunting task at first because of the steep and long learning curve of rust as a programming language.

We know that contacting with REST APIs is something that we come across creating almost any other app that comes to mind.

We’ll make use of the reqwest library to make our request that is essentially a higher level implementation of the default HTTP client.

# Cargo.toml
reqwest = { version = "0.11", features = ["json"] }
tokio = { version = "1", features = ["full"] }
dotenv = "0.15.0" # optional
serde = {version = "1.0.144", features = ["derive"]}

We import the following libraries to make this work

LibraryPurpose
reqwestto make our requests
tokioto make async requests and other async stuff.
serdeto deserialize the json response into a rust struct

Making Basic GET Requests

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let resp = reqwest::get("<url>").await?;
    let resp_json = resp.json::<HashMap<String, String>>().await?;

    println!("{:#?}", resp_json);

    Ok(());
}

Here we do the following key things:

  • We add the tokio::main attribute to our main function to use await within our function.
  • We change the return type of the main function from unit type - () to a Result<(), Box<dyn std::error::Error>> to catch errors from the request if any.
  • Then we make the request using the get function and await on that, and also we use the turbofish operator to only get the return type of the future
    • More info on why we are using this operator, here.
  • Then we deserialize the JSON response to a HashMap<String, String> for convenience and await on that.

Adding headers to our request

To add headers to our request, we first make a request client and use that to add headers to our application.

use reqwest::header::HeaderMap;

#[tokio::main]
async fn main() {
    ...
    let client = reqwest::Client::new();
    let mut headers = HeaderMap::new();
    headers.insert("content-type", "application/json".parse().unwrap());
use reqwest::header::HeaderMap;

#[tokio::main]
async fn main() {
    ...
    let client = reqwest::Client::new();
    let mut headers = HeaderMap::new();
    headers.insert("content-type", "application/json".parse().unwrap());
    headers.insert("Authorization", format!("Bearer {}", API_TOKEN).parse().unwrap());

    let resp = client.get("<url>")
                                .headers(headers)
                                .send()
                                .await?;

    ...
}

Here’s what we did above:

  • We create a request client to send our request.
  • We create a mutable instance of the HeaderMap Instance that is similar to HashMap
  • We insert our headers as key-value pairs to the header.
    • We use .parse().unwrap() on the &str to convert the string type to the header value type.
  • We then add our headers to the client request by using the .headers() method.
  • Also, one thing different from directly using the get method is that we have to call the send method on our request before awaiting on it.

Sending post request with JSON body

We send the post request by using the post method on the request client or directly from the library and use the json method to add body to the post request.

The body here is just a HashMap in rust.

...
let mut body = HashMap::new();
body.insert("username", "myusername");
let resp = client.post("<url>")
    .json(&body)
    .send()
    .await?;
...

Deserializing the JSON response

We can deserialize the JSON response from the API by using the json method on the sent request, and get that into our preferred shape or type by calling it generically with that type.

One thing to note is that the type must implement the Deserialize trait.

First thing first we create the type we want our JSON response in and implement the deserialize trait on it, implementing the trait ourselves is tedious and unreliable, so we use the serde library that we imported before do that for us.

use serde::Deserialize;

// deriving the Debug trait also to be able to print it
#[derive(Debug, Deserialize)]
struct APIResponse {
    message: String;
    error: String;
}

We can then use the above struct to deserialize our response as:

...
let resp_json = resp.json::<APIResponse>().await?;
...

References