A Simple Rust Rocket Service

In my quest to build up my understanding of Rust I decided to play with Rocket the web framework for Rust, but I had to find inspiration on what to actually do in Rocket. As this was my first time using it Rocket I just wanted something small and easy to work with but will give me a good starting point to work with, and then I saw Kanye Rest.

Kanye Rest is a fairly small interesting web service that gets random quotes, it uses a static JSON file hosted on Github and provided me a good jumping off point to achieve something similar with my own Rust microservice. The overall concept was pretty simple it just is one route that will fetch the JSON file and select a random quote and serve it to the client, it provided some good challenges but not too much to end up taking weeks to implement and in the end it only took a weekend.

The Rocket getting started guide is pretty good at providing the basic setup needed to get up and running. My Cargo file ended up looking like this:

[dependencies]
rocket = "0.4.2"
rocket_contrib = "0.4.2"
rocket_cors = "0.5.1"
rand = "0.7.3"
reqwest = "0.9.18"
serde = "*"
serde_json = "*"
serde_derive = "*"

The three rocket crates are used to setup rocket along with work with Json responses and then of course everyone's favourite CORs! Which proved to be another problem as well, as always.

rand is used to generate random numbers needed to get the random quote I will be needing.

reqwest is a crate that allows for HTTP requests, I used the v0.9 here as v0.10 changes the interface quite a lot and decided to work with what I know instead of getting lost in figuring that out. This is what is used to make the request to the github raw JSON page.

The serde packages main aim is work with the JSON files and allow serilization and deserilization within Rust.

The Service

The first part of the service in Rust is to create the GET request to the Github raw file to pull in the JSON data:

fn get_quotes() -> Result<Vec<String>, Box<dyn std::error::Error>> {
    let response: QuoteApi = reqwest::get(QUOTE_URL)?.json()?;
    let quotes = response.quotes;

    Ok(quotes)
}

This function will pull the json file from the url endpoint and convert it to JSON, the function is wrapped to support the correct results passed out in the Ok function using the Vec type which is a safe undefined length array which is exactly what we need here there is also an error response just in case.

There is also the structs I use with the service:

#[derive(Serialize)]
struct QuoteJsonOutput {
    quote: String,
}

#[derive(Serialize, Deserialize, Debug)]
struct QuoteApi {
    quotes: Vec<String>,
}

These structs here are pretty simple but can be serialized by Rust and just represent the JSON file we get from github in the QuoteApi and just the output with the QuoteJsonOutput.

Next is the endpoint/ update the existing one this is fairly simple as it is just an unprotected GET endpoint:

#[get("/")]
fn index() -> Json<QuoteJsonOutput> {
    let quote_list: Vec<String> = get_quotes().unwrap();
    let quote_len = quote_list.len();
    let number = thread_rng().gen_range(0, quote_len);
    let output = format!("{}", quote_list[number]);

    let test = QuoteJsonOutput {
        quote: String::from(output),
    };

    Json(test)
}

This is also fairly simple it will output the desired JSON structure but will first pull the request then get its lengh as we don't know initally, then using rand we are able to get a random number between the limited of the array. A new string is then made with the format before putting the value into the output JSON structure and passing it into the JSON at the end to output.

and finally the server config with CORs:

fn main() -> Result<(), Error> {
    let allowed_origins = AllowedOrigins::All;
    let cors = rocket_cors::CorsOptions {
        allowed_origins,
        allowed_methods: vec![Method::Get].into_iter().map(From::from).collect(),
        allowed_headers: AllowedHeaders::some(&["Authorization", "Accept"]),
        allow_credentials: true,
        ..Default::default()
    }
    .to_cors()?;

    rocket::ignite()
        .mount("/", routes![index])
        .attach(cors)
        .launch();

    Ok(())
}

This bit was a little tricky, as CORs tends to be really but generally the setup is fairly following the default but I allow for all origins to not restrict this API at all and follow the default configuration after that. This CORs configuration is then attached to the rocket service along with the endpoint to create the complete server.

Retrospective

I learnt a lot about working with Rust from this small project, I am also happy I was able to get it done in just a few days and work on some new things, I also dockerized the project and enabled CD on the pipeline to allow for easier updates. The only issue I found was when the docker container was building the Rust program on the server it did use a lot of resources and that is just for a small project like this so I am not sure how resource heavy bigger builds will be and how much time it will take to compile, which is a common issue with Rust.

This was a nice start and I aim to do more Rocket work soon, something a bit bigger I think!

Photo by Yuliya Kosolapova on Unsplash