URL Shortener with Rust, Svelte, & AWS (5/): Frontend

URL Shortener with Rust, Svelte, & AWS (5/): Frontend

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 + Bulma, and then integrate it into our Rust code + Dockerfile. I will be using the Yarn 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.

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 localhost:3000 you should see a simple message: template welcome page

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.

yarn add --dev @sveltejs/adapter-static@next

Next, we need to change the svelte.config.js file to configure the adapter. Import the adapter...

import adapter from '@sveltejs/adapter-static';

...then update config.kit.

{
    // 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:

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

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.

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


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

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.min.css">

We will be using 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.

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

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

<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

That's all for this post! If you have any issues, make sure to check out the part-5 tag 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:

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!