DEV Community

Henry Boisdequin
Henry Boisdequin

Posted on

How to Fetch a Web API with Rust 🦀

In this tutorial, I will be teaching you how to fetch a Web API asynchronously in Rust. This tutorial is for developers who know the basics of Rust, have Rust installed, and want to learn about asynchronous programming in Rust. If you don't have Rust installed you can download it here: https://www.rust-lang.org/tools/install.

What are we building?

In this tutorial, we are doing to be building a CLI stocks app. We would run our program and specify an argument: the company symbol. For example: cargo run AAPL would give us the price of the Apple stock. We are going to be using the Finnhub API for this project which is completely free. Let's dive right into it!

Setting up our project

Let's create a new Rust project and open it up in your favourite code editor (mine is VSCode).

cargo new stock-cli
cd stock-cli/
Enter fullscreen mode Exit fullscreen mode

Your project should look like this:

├── Cargo.toml
└── src
    └── main.rs
Enter fullscreen mode Exit fullscreen mode

Let's install our dependencies by typing this under the dependencies section in our Cargo.toml. Your Cargo.toml should look like this:

[package]
name = "stock-cli"
version = "0.1.0"
authors = ["Your Name"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
structopt = "0.3.21"
exitfailure = "0.5.1"
reqwest = { version = "0.11.0", features = ["json"] }
serde = "1.0.119"
serde_json = "1.0.61"
serde_derive = "1.0.119"
tokio = { version = "1.0.2", features = ["full"] }
Enter fullscreen mode Exit fullscreen mode

I'll explain what these dependencies are along the way. Next, we need our API key. Go to https://finnhub.io/register and create an account. Once signed in, go to https://finnhub.io/dashboard and you should see your API key. Keep this open, we will use it in this project. With that out of the way, let's get into coding!

Coding our project

Let's first get the argument that the user passes. We can do that by using the standard library's env module. Your src/main.rs should look like this:

use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();
    println!("{:?}", args);
}
Enter fullscreen mode Exit fullscreen mode

In this code, we are importing the standard library's env module, using the args() function to collect them, and printing them out in the debug format ({:?} means to debug the value). If you run this command: cargo run AAPL, should receive this:

["target/debug/stock-cli", "AAPL"]
Enter fullscreen mode Exit fullscreen mode

As you can see, the value we are interested in is the 2nd element in the vector or the vector at index 1 (Rust is 0 indexed). Let's handle the scenario where a user doesn't enter an argument:

src/main.rs

use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();
    let mut company: String = "AAPL".to_string();

    if args.len() < 2 {
        println!("Since you didn't specify a company symbol, it is defaulted to AAPL.");
    } else {
        company = args[1].clone();
    }

    println!("{:?}", company);
}
Enter fullscreen mode Exit fullscreen mode

As you can see, if the user doesn't enter the company symbol, it defaults to AAPL. Otherwise, the company is the argument they specified. If we run our program without any arguments, it should still work.

cargo run

Since you didn't specify a company symbol, it has defaulted to AAPL.
"AAPL"
Enter fullscreen mode Exit fullscreen mode

Now, let's fetch the Finnhub API. The API endpoint we are using is /quote which returns the current stock price and some other details.

src/main.rs

use exitfailure::ExitFailure;
use reqwest::Url;
use serde_derive::{Deserialize, Serialize};
use std::env;

#[derive(Serialize, Deserialize, Debug)]
struct CompanyQuote {
    c: f64,
    h: f64,
    l: f64,
    o: f64,
    pc: f64,
    t: i128,
}

impl CompanyQuote {
    async fn get(symbol: &String, api_key: &String) -> Result<Self, ExitFailure> {
        let url = format!(
            "https://finnhub.io/api/v1/quote?symbol={}&token={}",
            symbol, api_key
        );

        let url = Url::parse(&*url)?;
        let res = reqwest::get(url).await?.json::<CompanyQuote>().await?;

        Ok(res)
    }
}

#[tokio::main]
async fn main() -> Result<(), ExitFailure> {
    let api_key = "YOUR API KEY".to_string();
    let args: Vec<String> = env::args().collect();
    let mut symbol: String = "AAPL".to_string();

    if args.len() < 2 {
        println!("Since you didn't specify a company symbol, it has defaulted to AAPL.");
    } else {
        symbol = args[1].clone();
    }

    let res = CompanyQuote::get(&symbol, &api_key).await?;
    println!("{}'s current stock price: {}", symbol, res.c);

    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

Make sure to replace "YOUR API KEY" with the API key in your dashboard. In this code, we are outlining the response which is the struct CompanyQuote. In the implementation of CompanyQuote, we have an asynchronous function called get which returns a Result with a CompanyQuote or an error. This function expects the company symbol and API key as parameters. First, we are formatting our URL to contain the symbol and API key. Next, we are parsing the URL and getting it using the reqwest module. We are awaiting the response and then turn it into JSON. The ? operator just means the same as unwrap. Finally, we are returning an Ok with the result inside of it.

Firstly, before the main function, we are putting this: #[tokio::main]. That means that we can use await in our main function. Towards the end of the main function, we are getting the company quote and passing the symbol and API key as parameters. We await the response and then (using the println! macro) we print out the current stock price. Then we return an Ok saying that everything went as planned.

You might be wondering, what does async and await mean?

Async/await

In this section, I will give you a quick overview of async and await.

Async - allows you to do await
Await - waits for the command to execute before moving on to the next command

Why do we need to wait for the command to execute?

Fetching a web API takes some time, as you can imagine. If we don't wait for the data to be fetched and then do something with the response (like turning it into JSON) we will get an error because the result is a future (waiting to fetch the data). Further reading on async/await in Rust can be found here: https://dev.to/gruberb/explained-how-does-async-work-in-rust-46f8.

Running the code

Let's run the code by running cargo run {the company symbol you want the stock price of} (I did cargo run TSLA). As you can see, it works (the stock price is at the time of writing this article)!

TSLA's current stock price: 826.16
Enter fullscreen mode Exit fullscreen mode

I hope you learnt how to fetch an API with Rust and have a general understanding of async and await. More Rust tutorials coming soon! The final code is on GitHub. Thanks for reading!

GitHub logo henryboisdequin / stock-cli

A simple CLI app that gives you the current stock price of a company.

Henry

📰 Newsletter
🐱 GitHub
🐦 Twitter

Top comments (5)

Collapse
 
muxcmux profile image
🥔

Nice writeup.

I don't see why you need async in your program - all you do is hit an api once, wait for the result and print it on screen. This program should work exactly the same without async/await.

A better example would be to pass multiple company names and fetch the data concurrently.

Also I don't think the ? operator is the same as unwrap. unwrap will panic the current thread if the result an Err, while ? will propagate it to the call site.

Collapse
 
hb profile image
Henry Boisdequin

It's true that async is not needed but I wanted to show how to use async/await in Rust.

A better example would be to pass multiple company names and fetch the data concurrently.

Great idea! I will be sure to implement that type of tutorial soon. Also, the ? operator is not the same as unwrap, you're right. Since this post is more for beginners and it wasn't focused on the ? operator, I just wanted them to be familiar with the syntax and not have to worry about what it does.

Thanks for your input.

Collapse
 
didibear profile image
Adrien Turiot

Hey, what do you think of using unwrap_or for the optional parameter like in this Playground example here ?

Collapse
 
hb profile image
Henry Boisdequin

Wow, that looks great. Thanks for the suggestion!

Collapse
 
iarmankarimi profile image
Arman karimi • Edited

reqwest::get() makes a new http client on each call. use one client and get() from that if you are making many requests.