Creating a minimal RESTful song request API using Rocket ๐Ÿš€

Creating a minimal RESTful song request API using Rocket ๐Ÿš€

Get a step-by-step tutorial on creating a minimal RESTful song request API using the Rocket crate for Rust.

ยท

5 min read

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>

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"

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![])
}

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

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());

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");

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")
}

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())
}

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.
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.
Don't worry, we'll just create a new GET route.

Only one line of code!

#[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

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.

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.

#[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

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

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

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

Thanks for reading!

ย