Implementing a Web App With Rocket | by applied.math.coding | Apr, 2022

Harness the power of this mature Rust framework

In this story, I want to show you an example of how one can create a web application with a backend on top of the Rocket framework.

Moreover, we will containerize the app by building a docker image. The application is deemed to do something very simple. It will provide the visualization of a convex hull of random points as has been promised in my recent post here. If you are interested in the used algorithm you may consult the aforementioned article, but it is not inevitable necessary in order to understand what follows. If you are totally new to Rust, you might want to start with some introduction like here before reading this story.

Our goals in this story are as follows:

  1. creating a Rocket server and serving Graham’s scan algorithm by an API
  2. creating a browser-based front-end that requests the server to run the above algorithm for a set of random points
  3. hosting the front-end on the Rocket server as static resources
  4. creating a docker file that builds the front-end, the back-end and runs the server

Creating a server:

First of all, Rocket is a relatively mature framework for building web applications with Rust. It supports many features and enforces good and secure standards.

First, we create a project by doing…

cargo init --bin

…in some project folder. This creates the necessary Cargo.toml for us.

In order to be able to use Rocket, we will need to add the following dependencies:

...[dependencies]
serde = { version = "1.0.136", features = ["derive"] }
rocket = { version = "0.5.0-rc.1", features= ["json"] }

serde is being used to support de/serialization of request/response data and for rocket we set the feature json.

Features is a concept of cargo that works wonderful to reduce build sizes. In particular, the feature json enables the support for json data requests/responses and the derive feature of serde contains utilities that makes it easy to ‘derive’ de/serialization traits for our Rust data types. We will see this soon in action.

This code readily defines two endpoints:

  • GET /test that returns just the string hello world
  • POST /convex-hull that requires a JSON-encoded list of Points, calls the Graham scan algorithm on these points and returns the result back, that is, a JSON-encoded list of Points

In the latter endpoint, the macro contains the parameter data. This is supposed to glue together the result of the parsed request body with the variable points.

Claiming the function rocket annotated with #[launch] provides the actual main method. Here, an instance of rocket is build (the server), and several paths are getting mounted against the above endpoints.

The calls of mount like .mount("/api", routes![convex_hull, index]) make the endpoints defined in convex_hull resp. index available under the common route /api. The first call to mount binds an entire file system, that is /client/distunder the route /. This makes files that are placed in /client/dist directly serve-able. We are going to use that for hosting all static front-end assets, like index.htmletc… .

I think, easier it couldn’t be. Moreover, this structure resembles a typical modern way of setting up a server (see for instance Express).

Having all this, we could run the project as usually by cargo run and point the browser at http://localhost:8000/test to get the string hello world.

One more thing we have put into the main.rsthis is an e-2-e test for our endpoint:

In a larger project, you probably would put this into another file, but for our purposes, it can rest inside main.rs. Again, this is where Rust and Rocket do shine together. First of all, tests are being set up as usually, by just adding a module annotated with #[cfg(test)]. So we already know the test-runner that will run all the tests: cargo test.

Secondly, Rocket provides in a module local various utilities that support testing client requests against endpoints. It is this part, that creates an HTTP client and makes a POST request to /api/convex-hull:

// creating a client
let client = Client::tracked(super::rocket()).unwrap();
// making a POST request with JSON-encoded body
let response = client
.post("/api/convex-hull").json(&points).dispatch();

The remaining code is simply using the usual assertEq! to verify if the yielded response coincides with our expectations.

Creating the client

The particular client for this application is a Vue project built with Vite. Since the client-side is not our main focus in this story, I will not give much more details than the following.

As already mentioned above, the server is set to mount the route / against the directory /client/dist. For this reason, whatever the client is, its html, js, css resources must be all available in this folder.

Since I am using Vue together with TypeScript and Sass, a build step is required that produces all browser readable artifacts and places them into the mentioned folder.

Containerization (Docker)

Docker is a wonderful way to publish or deploy applications. For what follows it is assumed that the environment has installed some version of docker and that you have some basic of what docker is.

The docker file to multi-stage build the application is as follows:

FROM node:16.13.1 AS client
WORKDIR /app
COPY ./client .
RUN npm i
RUN npm run build
FROM rust:1.60 AS server
WORKDIR /app
COPY . .
COPY --from=client /app/dist ./client/dist
RUN cargo build --release
CMD ["./target/release/convex-hull"]

The first section is about building the front-end. This is done on an image that contains Node.js and runs all the build steps of Vite.

The second part is for building our server. The artifacts from the above front-end build are copied into this stage and then we instruct cargo to build the project: cargo build --release.

The final command ( CMD ) only is executed when we later run the image within a container.

This above Dockerfile is executed like this:

docker build -t convexhull .

Afterward, we can verify if the image has been created by doing

docker image ls

And finally, we can run the image and do a port mapping that ensures that from outside we are able to access the server:

docker run -it -p 8000:8000  convexhull

Having this, we can point our browser to http:\localhost:8000 And hopefully, the following not so overwhelming UI will appear:

Please note, that the above docker file only deems the purpose to quickly share a prototype project. It is far from being optimized and there exist many options to substantially decrease its size.

A very nice feature of docker is the docker-hub. If you create an account here, you are able to push the above-built image into a repository and share it with others. You just have to do the following steps:

First, create a new repository at docker-hub. Afterward do these steps:

docker login  // this will ask you to authenticatedocker tag convexhull:latest YOURACCOUNT/REPONAME:convexhulldocker push YOURACCOUNT/REPONAME:convexhull

This will push the image convexhull to the corresponding repository. Just make sure you replace YOURACCOUNT/REPONAME with you chosen names.

Having all that you can tell the persons you want to share your image with to do a

docker pull YOURACCOUNT/REPONAME:convexhulldocker run -it -p 8000:8000 YOURACCOUNT/REPONAME:convexhull

If you want to use mine, you can do this by

docker pull appliedmathcoding/convexhull:convexhull
docker run -it -p 8000:8000 appliedmathcoding/convexhull:convexhull

The entire project can be seen and cloned from this repository. Of course, this story only treats the basics of developing a web app with Rocket, but more is yet to come in further posts.

Thanks for reading!

Leave a Comment