URL Shortener with Rust, Svelte, & AWS (5/): Frontend
Tags: rust, svelte, aws, docker
Published: 2021-09-29 17:04:06

In the last post, we worked on containerizing the backend of our application, so that we can deploy it with a single command. In this post, we will create a static front-end application with [Svelte](https://svelte.dev/) + [Bulma](https://bulma.io/), and then integrate it into our Rust code + Dockerfile. I will be using the [Yarn](https://yarnpkg.com/) package manager, but feel free to use `npm` if you prefer.

### Cloning the Template

First, run the following command in the root of your repo. This will create a directory called `svelte` which contains a template that we'll use to build a simple frontend for our API.
```sh
npm init svelte@next svelte
```
Select the `Skeleton project` template, `Yes` to Typescript, and then `No` to all the other questions.

Inside the `svelte` directory, run `yarn install` to install dependencies, and then `yarn dev` to start a dev server. By going to http://localhost:3000/ you should see a simple message:
![template welcome page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jrnua1zzka9velle7acb.png)

### Adding a Static Adapter

Before we can deploy the site, we need to adapt it to our deployment target. SvelteKit provides a number of different adapters for platforms like Cloudflare Workers, Netlify, and Vercel. In our case, however, we will be using `adapter-static` to prerender our entire site. 

First, we need to install the adapter using Yarn.
```sh
yarn add --dev @sveltejs/adapter-static@next
```
Next, we need to change the `svelte.config.js` file to configure the adapter. Import the adapter...
```js
import adapter from '@sveltejs/adapter-static';
```
...then update `config.kit`.
```js
{
	// hydrate the <div id="svelte"> element in src/app.html
	target: '#svelte',
	// prerender the pages so they can be served statically
	adapter: adapter()
}
```
By running `yarn build`, you should see that the `/svelte/build` directory is populated with an `index.html` file (among some other files / folders).

### Serving with Rocket

Now that we have statically built our application, we can serve it with Rocket. To do so, simply change our launch function to the following:
```rust
#[launch]
fn rocket() -> _ {
    rocket::build()
        .manage(DashMap::<u32, String>::new())
        .mount("/", routes![shorten, redirect])
        .mount(
            "/",
            if cfg!(debug_assertions) {
                // debug mode, therefore serve relative to crate root
                FileServer::from(rocket::fs::relative!("/svelte/build"))
            } else {
                // dockerized, therefore serve from absolute path
                FileServer::from("/app/static")
            },
        )
}
```
The `if cfg!(debug...` statement will become clearer later on, when we update the Dockerfile.

### Fixing Tests

Unfortunately, you may get an email saying that your Github Actions tests have failed. If you look at the logs, it should be apparent that the `FileServer` failed to mount, because it couldn't find the provided directory. To fix this, we can add steps to install yarn, install dependencies, and then build the static site:
```yml
steps:
  - uses: actions/checkout@v2
  - name: Build Rust
    run: cargo build
  - name: Install Yarn
    run: npm install --global yarn
  - name: Install Dependencies
    run: yarn --cwd svelte install
  - name: Build Svelte
    run: yarn --cwd svelte run build
  - name: Run Tests
    run: cargo test
```
For the sake of completion, I also added a simple test to check that the static site is being served as expected.
```rust
#[test]
fn static_site() {
    let client = Client::tracked(rocket()).expect("valid rocket instance");
    let response = client.get("/").dispatch();
    assert_eq!(response.status(), Status::Ok);
}
```
Tests should now pass as expected.


### Updating the Dockerfile

Currently our Dockerfile uses two images - one to build the Rust project, and the second to run the executable. Now we need to add a third one for building the static site.

```docker

# ...
RUN cargo install --offline --path .

# use a node image for building the site
FROM node:16 as static

WORKDIR /svelte

COPY ./svelte .

RUN yarn install && yarn build

# use a slim image for actually running the container.
FROM rust:slim

# ...

COPY --from=build /usr/local/cargo/bin/aws-rust-api /usr/local/bin/aws-rust-api
COPY --from=static /svelte/build/ ./static

# ...
```

You should now be able to start your application using `docker-compose --build` and see your site when you go to http://127.0.0.1.

### Adding Functionality

Although our site is being served statically, we still don't have any functionality! First, we will add support for simple Bulma styling to the `app.html` `<head></head>` tags.
```html
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.min.css">
```
We will be using [SuperAgent](https://www.npmjs.com/package/superagent) for making API requests, so let's add it to our dependency list.
`yarn add superagent`
Next, we will create a `routes/__layout.svelte` file, which will wrap around any of the routes in the directory.
```html
<div id="svelte" class="container is-fluid my-5">
    <nav class="navbar is-dark" role="navigation">
        <div class="navbar-brand">
            <div class="navbar-item ml-5 is-dark">
                <img src="/favicon.png" width="32" height="32" alt="logo" />
            </div>
            <h1 class="title is-2 navbar-item">URL Shortener</h1>
        </div>
    </nav>

    <slot />
</div>
```
This will give us a simple, function-less navbar to go across the top of the screen.
Next, change the `index.svelte` file to contain the following script. 
```html
<script>
    import superagent from "superagent";

    let url = "";
    let request = null;

    function click() {
        request = superagent.post(`/api/shorten?url=${url}`);
    }

    function getUrl(key) {
        return `http://${window.location.host}/${key}`;
    }
</script>
```
Finally, we can bind to these variables / functions by appending the following to the `index.svelte` file:
```html
<div class="box">
    {#if request == null}
        <div class="field has-addons">
            <div class="control">
                <input
                    class="input"
                    type="text"
                    bind:value={url}
                    placeholder="URL"
                />
            </div>
            <div class="control">
                <button class="button is-info" on:click={click}>Shorten</button>
            </div>
        </div>
    {:else}
        {#await request}
            <p>Loading...</p>
        {:then response}
            <div class="card">
                <header class="card-header">
                    <p class="card-header-title">Done!</p>
                </header>
                <div class="card-content">
                    <a
                        class="content"
                        href={getUrl(response.text)}
                        target="_blank">{getUrl(response.text)}</a
                    >
                </div>
                <footer class="card-footer">
                    <button
                        class="card-footer-item button"
                        on:click={() => (request = null)}>Back</button
                    >
                    <button
                        class="card-footer-item button is-info"
                        on:click={() =>
                            navigator.clipboard.writeText(
                                getUrl(response.text)
                            )}>Copy</button
                    >
                </footer>
            </div>
        {:catch}
            <p>Something went wrong!</p>
        {/await}
    {/if}
</div>
```
The `bind:value={url}` is one of Svelte's special two-way bindings - updating the textbox will update the variable, and vice versa.

When the user clicks the button, the `click` function will start an asynchronous request to the API, and set the `request` variable to the uncompleted promise.

This will then cause the page to show `Loading...` until the request completes, at which point the shortened URL is displayed (with some buttons).

### Final Product

If you did everything correctly, you should be able to run `docker-compose up --build` and use your site at http://127.0.0.1!

![URL Shortener Demo](https://i.imgur.com/GmSKZx4.gif)

That's all for this post! If you have any issues, make sure to check out the [part-5 tag](https://github.com/mileswatson/url-shortener/tree/part-5) of my repo.

In the next post, we will cover the basics of EB, and set up a CD pipeline for automatically deploying your program to the cloud. 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!*