URL Shortener with Rust, Svelte, & AWS (2/): Simple HTTP API
Tags: aws, rust, svelte, docker
Published: 2021-09-20 20:32:32

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](TODO).

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](https://rocket.rs/) 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

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

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

```rust
#[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:

```sh
🚀 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](https://docs.rs/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`.

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

```rust
#[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](https://rocket.rs/v0.5-rc/guide/state/).

### 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](https://docs.rs/rand/) crate, so add the following to your dependency list.

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

```rust
#[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 <key> 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.

```rust
#[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:

```sh
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](https://github.com/mileswatson/url-shortener/tree/part-2) 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:
 - [DEV](https://dev.to/mileswatson)
 - [LinkedIn](https://www.linkedin.com/in/watsonmiles)
 - [Twitter](https://twitter.com/miles__watson)
 - [Github](https://github.com/mileswatson)

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!*