Creating a Server Side Rust WebAssembly App with Spin 2.0
Tags: rust, webassembly, programming, tutorial
Published: 2023-11-05 17:23:49


[Fermyon Spin](https://developer.fermyon.com/spin) is the [open source](https://github.com/fermyon/spin) 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](https://www.rust-lang.org/tools/install)
* The Wasm32-Wasi build target for rust (`rustup target add wasm32-wasi`)
* [Spin 2.0](https://developer.fermyon.com/spin/v2/install)

> If JavaScript is more your thing, I wrote a similar [tutorial on server side JS and WebAssembly](https://dev.to/technosophos/server-side-javascript-in-webassembly-4coh) using Spin 1.5. I also wrote a [tutorial on Prolog and Spin](https://dev.to/technosophos/prolog-writing-a-server-side-app-in-webassembly-using-spin-29nj) and   one on using [Spin 1.5 and Python](https://dev.to/technosophos/building-a-serverless-webassembly-app-with-spin-4nf0).

## Scaffolding a Spin 2.0 WebAssembly Project
Spin can generate an entire project for you. It supports a variety of languages, including [JavaScript](https://developer.fermyon.com/spin/v2/javascript-components), [TypeScript](https://developer.fermyon.com/spin/v2/javascript-components), [Python](https://developer.fermyon.com/spin/v2/python-components), and [Go](https://developer.fermyon.com/spin/v2/go-components). 

```console
$ 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        |
+------------------------------------------------------------------------+
```

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

```console
$ spin new rust-spin-app -t http-rust
Description: This is a description of the app
HTTP path: /...
```

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:

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

1 directory, 3 files
```

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:

```toml
[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]
```

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:

```toml
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"]
```

This format is [well documented](https://developer.fermyon.com/spin/v2/manifest-reference), 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:

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

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.

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

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`.
```toml
[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"]
```

In WebAssembly, a _component_ is a WebAssembly binary that conforms to the [WebAssembly Component Model](https://component-model.bytecodealliance.org/introduction.html) 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:

```rust
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")?)
}
```

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

```console
$ 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)
```

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:

```console
$ curl localhost:3000/
Hello, Fermyon
```

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:

```rust
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
}
```

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:

```rust
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")?)
}
```

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:

```console
$ 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
```
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:

```rust
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>")?)
}
```

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"](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g2w9s157qd9b0qnvhnb2.png)

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:

```console
$ 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)
```

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](https://developer.fermyon.com/spin/v2/kubernetes).

![Browser pointing to the Fermyon Cloud URL and showing "Hi There"](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s366bn2leaozbdjy5ltq.png)

## 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](https://developer.fermyon.com/spin/quickstart) to try out other languages.

In follow-ups, I’ll cover some of the other features like using [key value storage](https://developer.fermyon.com/spin/v2/key-value-store-tutorial),  built-in [SQLite](https://developer.fermyon.com/spin/v2/sqlite-api-guide), or [AI inferencing with LLaMa2 and Code Llama](https://developer.fermyon.com/spin/v2/serverless-ai-api-guide). In the meantime, if you are looking for inspiration or more examples, the [Spin Up Hub](https://developer.fermyon.com/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"_