DEV Community

Cover image for Creating a Server Side Rust WebAssembly App with Spin 2.0
Matt Butcher
Matt Butcher

Posted on

Creating a Server Side Rust WebAssembly App with Spin 2.0

Fermyon Spin is the open source tool for building serverless functions with WebAssembly. We’re going to use a few Spin commands to go from blinking cursor to deployed app in just a few minutes. Along the way, we’ll walk through a Spin project and see some of the features of Spin 2.0.

Prerequisites

You’ll need a few things to follow along:

  • Rust
  • The Wasm32-Wasi build target for rust (rustup target add wasm32-wasi)
  • Spin 2.0

If JavaScript is more your thing, I wrote a similar tutorial on server side JS and WebAssembly using Spin 1.5. I also wrote a tutorial on Prolog and Spin and one on using Spin 1.5 and Python.

Scaffolding a Spin 2.0 WebAssembly Project

Spin can generate an entire project for you. It supports a variety of languages, including JavaScript, TypeScript, Python, and Go.

$ spin templates list
+------------------------------------------------------------------------+
| Name                Description                                        |
+========================================================================+
| http-c              HTTP request handler using C and the Zig toolchain |
| http-empty          HTTP application with no components                |
| http-go             HTTP request handler using (Tiny)Go                |
| http-grain          HTTP request handler using Grain                   |
| http-js             HTTP request handler using Javascript              |
| http-php            HTTP request handler using PHP                     |
| http-prolog         HTTP request handler using Trealla Prolog          |
| http-py             HTTP request handler using Python                  |
| http-rust           HTTP request handler using Rust                    |
| http-swift          HTTP request handler using SwiftWasm               |
| http-ts             HTTP request handler using Typescript              |
| http-zig            HTTP request handler using Zig                     |
| php                 HTTP PHP environment                               |
| redirect            Redirects a HTTP route                             |
| redis-go            Redis message handler using (Tiny)Go               |
| redis-rust          Redis message handler using Rust                   |
| static-fileserver   Serves static files from an asset directory        |
+------------------------------------------------------------------------+
Enter fullscreen mode Exit fullscreen mode

For this example, we’ll use Rust. And we’re building an HTTP responder, so we want http-rust.

$ spin new rust-spin-app -t http-rust
Description: This is a description of the app
HTTP path: /...
Enter fullscreen mode Exit fullscreen mode

The spin new command takes the name of our project (rust-spin-app). I also used the -t (—template) option to tell it to scaffold based on the http-rust template.

It prompted me for two pieces of information:

  • Description is a developer-facing description of the project. After the command is done, you’ll see it in spin.toml
  • HTTP Path is the relative path in the URL that this app will live on. /..., the default, means “answer on / and any other paths under root”.

One app can listen on multiple HTTP paths. To add more, edit the spin.toml file.

At this point, we have a newly created directory rust-spin-app with some files in it:

$ tree rust-spin-app/
rust-spin-app/
├── Cargo.toml
├── spin.toml
└── src
    └── lib.rs

1 directory, 3 files
Enter fullscreen mode Exit fullscreen mode

We can change directories to that newly created directory and start working. cd rust-spin-app.

  • Cargo.toml is the Cargo configuration that most Rust projects use.
  • spin.toml is Spin’s configuration file
  • src/lib.rs is the source file we’ll be working with in a moment.

A Glance at Cargo.toml

If we open up the Cargo.toml file, we’ll see this:

[package]
name = "rust-spin-app"
authors = ["Matt Butcher <matt.butcher@fermyon.com>"]
description = "This is a description of the app"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = [ "cdylib" ]

[dependencies]
anyhow = "1"
http = "0.2"
spin-sdk = "v2.0.0"

[workspace]
Enter fullscreen mode Exit fullscreen mode

Note that spin new declared dependencies on anyhow, http, and spin-sdk. All of these are used by the code stubbed out in src/lib.rs

You’ll notice the description was completed with the Description we typed when running spin new.

Next let’s look at spin.toml

The spin.toml File

Next let’s look at the spin.toml file:

spin_manifest_version = 2

[application]
name = "rust-spin-app"
version = "0.1.0"
authors = ["Matt Butcher <matt.butcher@fermyon.com>"]
description = "This is a description of the app"

[[trigger.http]]
route = "/..."
component = "rust-spin-app"

[component.rust-spin-app]
source = "target/wasm32-wasi/release/rust_spin_app.wasm"
allowed_outbound_hosts = []
[component.rust-spin-app.build]
command = "cargo build --target wasm32-wasi --release"
watch = ["src/**/*.rs", "Cargo.toml"]
Enter fullscreen mode Exit fullscreen mode

This format is well documented, but we’ll walk through the basics.

First, with Spin 2, the manifest version has changed from 1 to 2. Spin 2 still understands the older 1 version (so you don’t have to go updating all your old Spin projects), but version 2 has all the new stuff.

The TOML file starts with a definition of the application, which is mainly metadata about the application itself:

[application]
name = "rust-spin-app"
version = "0.1.0"
authors = ["Matt Butcher <matt.butcher@fermyon.com>"]
description = "This is a description of the app"
Enter fullscreen mode Exit fullscreen mode

You’ll notice that name and description came from our spin new command. The first version is always 0.1.0. And authors is pulled (I think) from git. You can edit any of these. Just note that changing the name will change the name of the binaries. For example, when we do a spin build, it will generate a WebAssembly file named rust-spin-app.wasm. You will also want to keep name synced with your Cargo.toml.

Next, the spin.toml defines an HTTP trigger, to which it assigns one component. In TOML syntax, [[SOMETHING]] declares that this is an item in a list named SOMETHING. So [[trigger.http]] states that we are declaring the first HTTP trigger in this app. As I mentioned previously, we can declare more than one HTTP trigger per app. But for this example we only need one.

[[trigger.http]]
route = "/..."
component = "rust-spin-app"
Enter fullscreen mode Exit fullscreen mode

This section defines the route /... (anything under root) and maps it to a component named rust-spin-app.

Route matching goes by specificity. So if we defined a second route name /foo, then a call to /foo would use that component, while all other routes would still match our /... wildcard.

Next, we define one component named rust-spin-app.

[component.rust-spin-app]
source = "target/wasm32-wasi/release/rust_spin_app.wasm"
allowed_outbound_hosts = []
[component.rust-spin-app.build]
command = "cargo build --target wasm32-wasi --release"
watch = ["src/**/*.rs", "Cargo.toml"]
Enter fullscreen mode Exit fullscreen mode

In WebAssembly, a component is a WebAssembly binary that conforms to the WebAssembly Component Model specification. All Spin 2 apps are component-based.

In this section, we tell Spin a few things about our project and how it should create and use our component:

  • source: The component source is a path to the Wasm binary for this component. spin new correctly calculated this for the location where the Rust toolchain will place the .wasm file during a cargo build
  • allowed_outbound_hosts is empty, but if you are allowing your component to make outbound HTTP requests, you would need to specify what it is allowed to contact.
  • The component.rust-spin-app.build section tells spin build what to do.
    • For a Rust project, it runs cargo build with a few flags set. Using --release strips debugging symbols out of the Wasm binary, and makes the wasm file much smaller and faster to load.
    • watch specifies which files the spin watch command should test for changes. The spin watch command lets you code away while spin auto-builds and keeps local copy running.

Our spin.toml is pretty basic, and we won’t need to edit anything before building and running our app. So let’s go take a look at the code.

Coding a Component

Inside src/lib.rs, the spin new command has scaffolded out a single function for us:

use spin_sdk::http::{IntoResponse, Request};
use spin_sdk::http_component;

/// A simple Spin HTTP component.
#[http_component]
fn handle_rust_spin_app(req: Request) -> anyhow::Result<impl IntoResponse> {
    println!("Handling request to {:?}", req.header("spin-full-url"));
    Ok(http::Response::builder()
        .status(200)
        .header("content-type", "text/plain")
        .body("Hello, Fermyon")?)
}
Enter fullscreen mode Exit fullscreen mode

This is an entire Spin app. So without changing a thing, we can run spin build --up and see the result of this:

$ spin build --up                                      
Building component rust-spin-app with `cargo build --target wasm32-wasi --release`
    Finished release [optimized] target(s) in 0.37s
Finished building all Spin components
Logging component stdio to ".spin/logs/"

Serving http://127.0.0.1:3000
Available Routes:
  rust-spin-app: http://127.0.0.1:3000 (wildcard)
Enter fullscreen mode Exit fullscreen mode

The spin build reads the spin.toml and runs whatever is specified in the [component.rust-spin-app.build] ’s command directive. We saw already that this will run a cargo build. Then the —up flag will start a local server serving our app. This is the same as running spin build followed by running spin up. Running a quick curl command, we can see the result:

$ curl localhost:3000/
Hello, Fermyon
Enter fullscreen mode Exit fullscreen mode

Now let’s go back to the code and see what is happening. Spin apps follow the pattern sometimes called “event-driven programming” and sometimes called “serverless functions.” Essentially, in practice, a function is mapped to an event. Whenever the platform encounters that event, it will run the program, using the mapped function as its entrypoint.

So instead of writing (or running) a server process (a daemon), we just write a function. Thus the name “serverless function.”

In Rust, the function declaration looks like this:

use spin_sdk::http::{IntoResponse, Request};
use spin_sdk::http_component;

#[http_component]
fn handle_rust_spin_app(req: Request) -> anyhow::Result<impl IntoResponse> {
    // your code
}
Enter fullscreen mode Exit fullscreen mode

The http_component macro tells rust this is an HTTP component. Then we implement a function that takes an HTTP Request and returns a Result that contains something it can turn into an HTTP Response. If you think back to the Cargo.toml, we declared three dependencies: http, spin-sdk, and anyhow. You can see in this code why we need those three.

Let’s take a look at the function body:

use spin_sdk::http::{IntoResponse, Request};
use spin_sdk::http_component;

/// A simple Spin HTTP component.
#[http_component]
fn handle_rust_spin_app(req: Request) -> anyhow::Result<impl IntoResponse> {
    println!("Handling request to {:?}", req.header("spin-full-url"));
    Ok(http::Response::builder()
        .status(200)
        .header("content-type", "text/plain")
        .body("Hello, Fermyon")?)
}
Enter fullscreen mode Exit fullscreen mode

The function is doing two things.

First, println() is printing a message to STDOUT. Running spin up, STDOUT will be mapped to your console window. If you spin deploy to Fermyon Cloud, that same message would go to your cloud log.

The second thing the above is doing is returning a Result wrapping an http::Response. The response has a status code 200 (which is the HTTP status code for success), then sets the content type to text/plain, and sets the body to Hello, Fermyon. If we re-ran the curl command with the verbosity turned up, we can see all three of those things:

$ curl -v localhost:3000                     
*   Trying 127.0.0.1:3000...
* Connected to localhost (127.0.0.1) port 3000 (#0)
> GET / HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/8.1.2
> Accept: */*
> 
< HTTP/1.1 200 OK
< content-type: text/plain
< transfer-encoding: chunked
< date: Fri, 03 Nov 2023 22:43:31 GMT
< 
* Connection #0 to host localhost left intact
Hello, Fermyon
Enter fullscreen mode Exit fullscreen mode

The response begins with HTTP/1.1 200 OK, where 200 is the status code we set. Then you can see our content-type: text/plain as well as our Hello, Fermyon message in the body.

And just for the sake of completeness, let’s do a small change here and then recompile:

use spin_sdk::http::{IntoResponse, Request};
use spin_sdk::http_component;

/// A simple Spin HTTP component.
#[http_component]
fn handle_rust_spin_app(req: Request) -> anyhow::Result<impl IntoResponse> {
    println!("Handling request to {:?}", req.header("spin-full-url"));

    // We'll return some HTML instead of plain text
    Ok(http::Response::builder()
        .status(200)
        .header("content-type", "text/html")
        .body("<html><body><h1>Hi there</h1></body></html>")?)
}
Enter fullscreen mode Exit fullscreen mode

Now we’re returning HTML, which means changing content-type in addition to change the body.

Re-running spin build --up, we can use curl again or point a web browser at our page:
Browser pointing to 127.0.0.1 and showing "Hi There"

Finally, if you want to deploy this to somewhere public, the easiest thing to do is use spin deploy, which will deploy your code to Fermyon Cloud and then give you back a public URL:

$ spin deploy    
Uploading rust-spin-app version 0.1.0 to Fermyon Cloud...
Deploying...
Waiting for application to become ready......... ready
Available Routes:
  rust-spin-app: https://rust-spin-app-dzlirkwf.fermyon.app (wildcard)
Enter fullscreen mode Exit fullscreen mode

You can now test out my app at https://rust-spin-app-dzlirkwf.fermyon.app

Spin apps can also be deployed in a variety of other places, including Kubernetes.

Browser pointing to the Fermyon Cloud URL and showing "Hi There"

Conclusion

This has been a quick walk through the process of building a WebAssembly app with Spin 2. You can head over to the Spin QuickStart guide to try out other languages.

In follow-ups, I’ll cover some of the other features like using key value storage, built-in SQLite, or AI inferencing with LLaMa2 and Code Llama. In the meantime, if you are looking for inspiration or more examples, the Spin Up Hub has lots of them. You can contribute your own examples and content there, too!


Image generated by Dall-E 3 via Bing Image Creator. My prompt: "A WebAssembly logo in the style of Dali's Persistence of Time"

Top comments (1)

Collapse
 
mortylen profile image
mortylen

Very well and clearly explained.