Creating a minimal RESTful song request API using Rocket 🚀

zofia

Zofia

Posted on October 19, 2022

Creating a minimal RESTful song request API using Rocket 🚀

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 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 crate.

Initializing the project

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

cargo init <YOUR_PROJECT_NAME>
Enter fullscreen mode Exit fullscreen mode

Adding dependencies

Add rocket as a dependency in your Cargo.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"
Enter fullscreen mode Exit fullscreen mode

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.

//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![])
}
Enter fullscreen mode Exit fullscreen mode

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

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
Enter fullscreen mode Exit fullscreen mode

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.

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.

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

static SONG_QUEUE: Mutex<LinkedList<String>> = Mutex::new(LinkedList::new());
Enter fullscreen mode Exit fullscreen mode

To learn more about Mutex, click here.

Utility function

We will be repeating the following code a lot:

let lock = SONG_QUEUE
    .lock()
    .expect("Unable to acquire lock on song queue because the Mutex was poisoned");
Enter fullscreen mode Exit fullscreen mode

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

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")
}
Enter fullscreen mode Exit fullscreen mode

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.

#[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())
}
Enter fullscreen mode Exit fullscreen mode

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

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

Creating the view route

Users can now add songs, but are unable to view which songs are currently in the queue.

Don't worry, we'll just create a new GET route.

Only one line of code!

#[get("/view")]
fn view() -> String {
    format!("{:?}", acquire_queue())
}
Enter fullscreen mode Exit fullscreen mode

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

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

Result

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"]
Enter fullscreen mode Exit fullscreen mode

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.

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

#[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())
}
Enter fullscreen mode Exit fullscreen mode

Result

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"]
Enter fullscreen mode Exit fullscreen mode

60 seconds later...

C:\Users\dev>curl http://localhost:8000/view
[]
Enter fullscreen mode Exit fullscreen mode

That's about it! Writing RESTful APIs in Rust are ridiculously easy with the rocket crate. Make sure to follow me on dev.to to be notified whenever I post a new article.

Thanks for reading!

💖 💪 🙅 🚩
zofia
Zofia

Posted on October 19, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related