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 http://localhost:3000/ you should see a simple message:
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!
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!
Top comments (0)