Creating a minimal RESTful song request API using Rocket 🚀
Tags: rust, api, rocket
Published: 2022-10-19 20:07:20

## What is a song request system?

If you watch streamers on platforms such as YouTube and Twitch, you may have encountered a song request system. A song request system allows the viewer to add a song to a  *song queue*. When the song reaches the front of the queue, the song is played over the live stream.

With the help of the [rocket](https://rocket.rs) crate, creating an API for this system in Rust is quite easy.

## Getting started

For this project, I'll be creating a RESTful API using the [rocket](https://rocket.rs) crate.

## Initializing the project

To initialize a new Cargo project, run the following command:

```
cargo init <YOUR_PROJECT_NAME>
```

## Adding dependencies

Add [rocket](https://rocket.rs) as a dependency in your `Cargo.toml`:

```toml
[dependencies]

# NOTE: This is a pre-release version.
# Thus, It is suggested NOT to use this in production.
rocket = "0.5.0-rc.2"
```

## Main function

With our Cargo project ready, we can remove the default main function entirely. This will make room for our new `rocket` method, which is attributed with Rocket's `launch` procedural macro.

This function will essentially replace our main function, and will be called on startup.

```rs
//main.rs

#[macro_use]
extern crate rocket;

use rocket::{Build, Rocket};

#[launch]
fn rocket() -> Rocket<Build> {
    Rocket::build()
        // Set the `/` route path as the base for our routes.
        // When we create our routes, we'll include them in the arguments for the `routes!` macro.
        .mount("/", routes![])
}
```

If we were to run this, we would see the following:

```txt
Configured for debug.
   >> address: 127.0.0.1
   >> port: 8000
   >> workers: 6
   >> ident: Rocket
   >> limits: bytes = 8KiB, data-form = 2MiB, file = 1MiB, form = 32KiB, json = 1MiB, msgpack = 1MiB, string = 8KiB
   >> temp dir: C:\Users\dev\AppData\Local\Temp\
   >> http/2: true
   >> keep-alive: 5s
   >> tls: disabled
   >> shutdown: ctrlc = true, force = true, grace = 2s, mercy = 3s
   >> log level: normal
   >> cli colors: true
Fairings:
   >> Shield (liftoff, response, singleton)
Shield:
   >> X-Content-Type-Options: nosniff
   >> Permissions-Policy: interest-cohort=()
   >> X-Frame-Options: SAMEORIGIN
Rocket has launched from http://127.0.0.1:8000
```

Now that we have our `rocket` function, we can start developing the functionality for our API.

## Storing the song queue

For storing the song queue, we will use a static `LinkedList`.<br>
We use a `LinkedList` because it's included in the standard library, and basically acts as a queue with extra features.

Though, to allow the list to be modified statically, we must wrap the list in a `Mutex`.

```rs
use std::collections::LinkedList;
use std::sync::Mutex;

static SONG_QUEUE: Mutex<LinkedList<String>> = Mutex::new(LinkedList::new());
```

<sub>To learn more about `Mutex`, click [here](https://fongyoong.github.io/easy_rust/Chapter_43.html).</sub>

## Utility function

We will be repeating the following code a lot:

```rs
let lock = SONG_QUEUE
    .lock()
    .expect("Unable to acquire lock on song queue because the Mutex was poisoned");
```

It's better if we create a function that does this for us.

```rs
use std::sync::{Mutex, MutexGuard};

fn acquire_queue<'a>() -> MutexGuard<'a, LinkedList<String>> {
    SONG_QUEUE
        .lock()
        .expect("Unable to acquire lock on song queue because the Mutex was poisoned")
}
```

## Creating the `add` route

With our `SONG_QUEUE` variable defined, we can start creating routes.

The `add` route will receive a **POST** request containing a song name, which will then be added to the song queue.

Optionally, we can return the position the song has been placed into.

```rs
#[post("/add/<song_name>")]
fn add_song(song_name: String) -> String {
    let mut lock = acquire_queue();
    
    lock.push_back(song_name);

    format!("Song added. This song is in position {}.", lock.len())
}
```

Don't forget to register this new route in the `routes!` macro.

1. `- .mount("/", routes![])`
2. `+ .mount("/", routes![add_song])`

To test this new route, run the program, then try curling our route with some parameters.

## Result

```txt
C:\Users\dev>curl -X POST http://localhost:8000/add/Hello
Song added. This song is in position 1.
```
```txt
C:\Users\dev>curl -X POST http://localhost:8000/add/Hello%20number%202
Song added. This song is in position 2.
```

## Creating the `view` route

Users can now add songs, but are unable to view which songs are currently in the queue.<br>
Don't worry, we'll just create a new **GET** route.

Only one line of code!

```rs
#[get("/view")]
fn view() -> String {
    format!("{:?}", acquire_queue())
}
```

Don't forget to register this new route in the `routes!` macro.

1. `- .mount("/", routes![add_song])`
2. `+ .mount("/", routes![add_song, view])`

## Result

```txt
C:\Users\dev>curl -X POST http://localhost:8000/add/Hello%20World
Song added. This song is in position 1.

C:\Users\dev>curl http://localhost:8000/view
["Hello World"]
```

## Removing songs

For the sake of the simplicity of the article, we won't actually play songs once they reach the front of the queue. Instead, we'll just remove songs after a certain amount of time has passed.

In this case, we'll remove songs 60 seconds after they've reached the front of the queue.

```rs
use std::thread;
use std::time::Duration;

fn remove_song_timer() {
    while !acquire_queue().is_empty() {
        thread::sleep(Duration::from_secs(60));
        acquire_queue().pop_front();
    }
}
```

We need to modify our `add_song` route to make sure a `remove_song_timer` thread is spawned if a song is added to an empty queue.

```rs
#[post("/add/<song_name>")]
fn add_song(song_name: String) -> String {
    let mut lock = acquire_queue();

    if lock.is_empty() {
        thread::spawn(remove_song_timer);
    }
    lock.push_back(song_name);

    format!("Song added. This song is in position {}.", lock.len())
}
```

## Result

```txt
C:\Users\dev>curl -X POST http://localhost:8000/add/Hello%20World
Song added. This song is in position 1.
C:\Users\dev>curl http://localhost:8000/view
["Hello World"]
```

60 seconds later...

```txt
C:\Users\dev>curl http://localhost:8000/view
[]
```

That's about it! Writing RESTful APIs in Rust are ridiculously easy with the [rocket](https://rocket.rs) crate. Make sure to follow me on `dev.to` to be notified whenever I post a new article.

Thanks for reading!