URL Shortener with Rust, Svelte, & AWS (2/): Simple HTTP API

URL Shortener with Rust, Svelte, & AWS (2/): Simple HTTP API

ยท

4 min read

In the first post of the series, I covered the reasons for choosing Rust and AWS, as well as the process for initialising a new Rust project. If you haven't followed the steps in that article, you can find it here.

In this article, we will create a simple URL-shortener API, and serve the endpoint locally. For the web framework, we will be using Rocket to reduce the amount of boilerplate and help us focus on the application logic.

Getting Started with Rocket

Before we can use Rocket, we need to add it to our list of dependencies (cargo.toml). We'll be using the JSON feature

[dependencies]
rocket = "0.5.0-rc.1"

To check everything is working, copy the following code to main.rs.

#[macro_use]
extern crate rocket;

#[get("/")]
fn index() -> &'static str {
    "Hello, world!"
}

#[launch]
fn rocket() -> _ {
    rocket::build().mount("/", routes![index])
}

When you start the program with cargo run, you should be presented with the following message:

๐Ÿš€ Rocket has launched from http://127.0.0.1:8000

If you open the link in a web browser, you should be presented with the expected "Hello, World!" message.

Concurrent HashMap

To store pairings of shortened URLs to full URLs, we will use a hashmap. However, we need to share this map across threads - those familiar with Rust will know that this can be quite tricky due to the restrictions of the borrow checker.

One approach to solving this would be to simply wrap a HashMap in a Mutex for controlling concurrent accesses, and then use an Arc for referencing counting. However, we can simplify this in two ways.

Firstly, we can use the dashmap crate for a fast, concurrent hashmap (DashMap implements Sync so it can be shared safely across threads). Although perhaps overkill for our use-case, dashmap provides better performance than naively using an RwLock. To install it, add the following dependency to cargo.toml.

dashmap = "4.0.2"

As for sharing this state across threads, we only need to access it from endpoints: therefore we can just let Rocket manage it directly.

#[launch]
fn rocket() -> _ {
    rocket::build()
        .manage(DashMap::<u32, String>::new())
        .mount("/", routes![index])
}

For more information on state in Rocket, you can check the Rocket docs.

Creating the endpoints

Now we need to create the actual endpoints to allow users to create and follow shortened URLs. For random number generation, we will use the rand crate, so add the following to your dependency list.

rand = "0.8.4"

The first endpoint will listen to POST requests of the form /api/shorten?url=___, and then generate a random url to return. It will return an error if the url field is empty, or a key if the URL was added to the hashmap.

Rocket will automatically parse the URL parameter and inject the managed hashmap.

#[post("/api/shorten?<url>")]
fn shorten(url: String, state: &State<DashMap<u32, String>>) -> Result<String, BadRequest<&str>> {
    if url.is_empty() {
        Err(BadRequest(Some("URL is empty!")))
    } else {
        let key: u32 = thread_rng().gen();
        state.insert(key, url);
        Ok(key.to_string())
    }
}

The other endpoint will listen to GET requests of the form /<key> where is a number. If the key exists in the hashmap, then it will redirect the user to the corresponding URL. Otherwise, it will return an error.

#[get("/<key>")]
fn redirect(key: u32, state: &State<DashMap<u32, String>>) -> Result<Redirect, NotFound<&str>> {
    state
        .get(&key)
        .map(|url| Redirect::to(url.clone()))
        .ok_or(NotFound("Invalid or expired link!"))
}

Remember to update your rocket function to mount the new routes!

Manual Testing

To check that your API is working as expected, you can use a tool like Postman or Curl to make POST requests, then enter the link manually in a browser. To use curl, try the following command:

curl -X POST -G --data-urlencode 'url=https://duck.com' http://localhost:8000/api/shorten

Warning: In PowerShell, curl is simply an alias for Invoke-WebRequest, therefore this command may not work without installing it manually.

You should see the endpoint respond with a number - use this in your browser to make the GET request. If your number was 123, for example, you should enter http://127.0.0.1:8000/123 into your address bar. You should be automatically redirected to whatever URL you set in the previous POST request.

If you are having any issues, check out the part-2 tag of my repo.

That's all for this post! In the next post, we will create a simple HTTP API with the Rocket web framework. Make sure to click the "Follow" button if you want to be alerted when the next part is available!

Footnote

If you enjoyed reading this, then consider dropping a like or following me:

I'm just starting out, so the support is greatly appreciated!

Disclaimer - I'm a (mostly) self-taught programmer, and I use my blog to share things that I've learnt on my journey to becoming a better developer. Because of this, I apologize in advance for any inaccuracies I might have made - criticism and corrections are welcome!