Although WebAssembly is an excellent fit for bringing complex solutions that need near-native runtime performance to the client (Browser), I see WebAssembly as a technology with way more potential and a way bigger impact on the server (read: cloud) in contrast. That said, WebAssembly will not replace containers on the server side. It’s an addition or alternative approach to packaging and distributing specific software components typically part of a bigger system. It’s on us - the developers and software architects - to decide if a container or WebAssembly is the better foundation for meeting a particular requirement.

We’re still in the early ages of WebAssembly (especially on the server side), where specs and ideas like WebAssembly System Interface (WASI) and WebAssembly Gateway Interface (WAGI) lay out the foundation for infrastructure frameworks application developers will build on. Besides WASI and WAGI, there is also the WebAssembly Component Model spec (the community is currently working towards the first release of the spec).

So still a lot of moving parts here. However, companies like Fermyon are pioneering in this area and building awesome ideas and technology to boost WebAssembly adoption on the server side. In this post, we will look at Spin, an open-source framework, runtime, and developer tooling created by Fermyon.



What is Fermyon Spin

Fermyon (founded in November 2021) does excellent pioneering work in the WebAssembly space. With Spin, they provide everything we need to build microservices. Spin is a holistic suite that consists of

  • Software Development Kit (SDK)
  • Runtime
  • Developer Toolchain

The Spin SDK is available for a wide range of programming languages that we can use to build our microservices. The SDK comes with batteries included that address everyday use cases like interacting with Redis and PostgreSQL or sending outbound HTTP requests. With Spin, we build microservices using the Web Assembly Component Model.

Although the team at Fermyon keeps on adding more and more languages, you can use any language that can be compiled to WebAssembly leveraging the WAGI executor provided by Spin.

Spin also acts as runtime, meaning that Spin loads and runs our custom microservices and instantiates them when the desired trigger is invoked. Speaking of triggers, Spin has two out-of-the-box triggers: HTTP and Redis. We can add more specialized triggers by individually extending Spin, as described in this article.

People at Fermyon are super focused on inner-loop performance. The’ spin’ CLI is everything we need to create and run applications. We use spin new to create a new microservice. We compile it with spin build (actually, spin build invokes the compiler of the chosen programming language as configured in the application manifest); Finally, we use spin up to run the microservice locally.

Getting started with Fermyon Spin

First, let’s install spin on our local development machine. We can download the latest release from the GitHub repository. If you have a local installation of Rust, you can clone the Spin repository and use cargo to install spin:

# clone the spin repository
git clone https://github.com/fermyon/spin -b v0.3.0
cd spin

# ensure you've the wasm32-wasi platform installed for rust
rustup target add wasm32-wasi

# Build & install spin
cargo install --path .

Let’s quickly verify the installation and invoke spin --version, which should (at the point of writing this article return spin 0.6.0 (with an additional git commit hash and the corresponding date).

Install the Spin starter templates

We use the spin CLI to create new projects. Upon creating a new project, we can choose from different templates to get started as fast as possible. To get access to those starter templates, we must install them by invoking spin templates install --git https://github.com/fermyon/spin. We can list all installed templates as shown here:

# see all installed templates
spin templates list

+-----------------------------------------------------------------+
| Name         Description                                        |
+=================================================================+
| http-c       HTTP request handler using C and the Zig toolchain |
| http-go      HTTP request handler using (Tiny)Go                |
| http-grain   HTTP request handler using Grain                   |
| http-rust    HTTP request handler using Rust                    |
| http-swift   HTTP request handler using SwiftWasm               |
| http-zig     HTTP request handler using Zig                     |
| redis-go     Redis message handler using (Tiny)Go               |
| redis-rust   Redis message handler using Rust                   |
+-----------------------------------------------------------------+

Hello World - The Spin Edition

Now that we have installed the spin CLI and the starter templates, we will build “Hello World” to give Spin a spin (SCNR 😁). The spin CLI will ask for necessary details about our hello world sample when we invoke spin new:

# move to your source directory (here ~/sources)
cd ~/sources

# create a new microservice in the ./hello-world folder
# use http-rust as template
# provide hello-world as the name
spin new -o ./hello-world http-rust hello-world

Project description: Hello World - The Spin Edition
HTTP base: /
HTTP path: /...

We’ll dive into things like HTTP base and routing capabilities for HTTP triggers later. For the sake of this demo, stick with the defaults. We end up with a bootstrapped Rust project and a manifest called spin.toml. The manifest is used for wiring up the Wasm component at runtime. However, let’s first revisit lib.rs:

use anyhow::Result;
use spin_sdk::{
  http::{Request, Response},
  http_component,
};

/// A simple Spin HTTP component.
#[http_component]
fn hello_world(req: Request) -> Result<Response> {
  println!("{:?}", req.headers());
  Ok(http::Response::builder()
    .status(200)
    .header("foo", "bar")
    .body(Some("Hello, Fermyon".into()))?)
}

As the generated sample outlines, we can use the spin SDK to construct an HTTP response based on information we find in the inbound HTTP request. This is more convenient than working with environment variables and STDOUT as we would have to do when using plain WAGI.

I bet you’re able to understand what the code does (no matter if you’ve ever used Rust before or not). For every inbound request, it will create an HTTP response with status code 200, add the HTTP header foo providing bar as the value and send Hello, Fermyon as the response body. The preceding code will also log all incoming HTTP request headers to STDOUT for demonstration purposes.

Besides the code, it’s also important to make yourself comfortable with the manifest. It contains essential metadata about the microservice and the invoked components based on the trigger (HTTP here). In real-world applications, you’ll lay out all your components that build the bigger system and provide both: essential metadata and configuration per component. You can find the following spin.toml in the project directory:

spin_version = "1"
authors = ["Thorsten Hans <[email protected]>"]
description = "Hello World - The Spin Edition"
name = "hello-world"
trigger = { type = "http", base = "/" }
version = "0.1.0"

[[component]]
id = "hello-world"
source = "target/wasm32-wasi/release/hello_world.wasm"
[component.trigger]
route = "/..."
[component.build]
command = "cargo build --target wasm32-wasi --release"

Running Hello World - The Spin Edition

We can quickly run the “Hello World” sample either using the combination of spin build and spin up, or we can take a shortcut using spin build --up:

# run hello world
cd ~/sources/hello-world

# build hello world
spin build

# run hello world
spin up

Serving http://127.0.0.1:3000
Available Routes:
 hello-world: http://127.0.0.1:3000 (wildcard)

Now, we can test “Hello World” by sending an HTTP request to http://127.0.0.1:3000 using the HTTP Client of your choice (curl here):

# call hello world
curl -iX GET http://127.0.0.1:3000

HTTP/1.1 200 OK
foo: bar
content-length: 14
date: Tue, 12 Jul 2022 07:29:11 GMT

Hello, Fermyon%

Are you already curious about performance? What about sending 10k requests with a concurrency of 50? We can do that with ease using tools like hey (https://github.com/rakyll/hey):

hey -c 50 -n 10000 http://127.0.0.1:3000

Summary:
 Total: 2.7089 secs
 Slowest: 0.0387 secs
 Fastest: 0.0003 secs
 Average: 0.0133 secs
 Requests/sec: 3691.5819

 Total data: 140000 bytes
 Size/request: 14 bytes

Response time histogram:
 0.000 [1]    |
 0.004 [394]  |■■■■■
 0.008 [990]  |■■■■■■■■■■■■■
 0.012 [2382] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
 0.016 [2998] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
 0.020 [2200] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
 0.023 [878]  |■■■■■■■■■■■■
 0.027 [110]  | 0.031 [38]   | 0.035 [7]    |
 0.039 [2]    |


Latency distribution:
 10% in 0.0070 secs
 25% in 0.0100 secs
 50% in 0.0134 secs
 75% in 0.0167 secs
 90% in 0.0196 secs
 95% in 0.0212 secs
 99% in 0.0245 secs

Details (average, fastest, slowest):
 DNS+dialup: 0.0000 secs, 0.0003 secs, 0.0387 secs
 DNS-lookup: 0.0000 secs, 0.0000 secs, 0.0021 secs
 req write: 0.0000 secs, 0.0000 secs, 0.0005 secs
 resp wait: 0.0133 secs, 0.0003 secs, 0.0387 secs
 resp read: 0.0000 secs, 0.0000 secs, 0.0006 secs

Status code distribution:
 [200] 10000 responses

Although the numbers are pretty impressive, it’s way more interesting that you won’t recognize any effect on your machine. This is way more interesting once you realize that every HTTP request results in Spin spawning a new instance of our Wasm-module 🤯.

HTTP-Trigger routing capabilities

When we generated Hello World - The Spin Edition with spin new, we were asked to provide the HTTP base and the HTTP path. HTTP base is configured globally on the scope of the HTTP trigger. In contrast, the HTTP path is component centric. Meaning we can have multiple components (commonly with different values for HTTP path as part of one Spin application).

Go and double-check the spin.toml file. You’ll find the default value for HTTP base as part of the HTTP-trigger configuration. HTTP path is located in the component configuration (see [[component]]).

Besides pinning specific components to dedicated routes (by providing the full path (e.g., api/dashboard), we can use the spread operator and tell Spin that a particular component will handle all requests having a path starting with a specific value.

[[component]]
id = "products"
source = "target/wasm32-wasi/release/products.wasm"
[component.trigger]
route = "/api/products/..."

The component configuration shown here tells Spin to instantiate and invoke the products component for all requests to /api/products (including api/products/archived, api/products/trending/byCategory, …).

When we register more than one component for the same route, the last component will receive requests at runtime.

Dealing with HTTP methods in Spin

Now that we walked through the basics of Spin and run our very first application, we’re going to extend the sample a bit. We will enhance the Hello World microservice to address the following requirements:

  • Accept only HTTP GET and POST requests
  • Respond with HTTP 405 when receiving requests using different HTTP methods
  • Respond with Hello, <NAME> where <NAME> is pulled from the request body (JSON) when receiving an HTTP POST request
  • Respond with Hello, <NAME> where <NAME> is pulled from the query string when receiving an HTTP GET request
  • Respond with Hello, Fermyon as fallback

Matching HTTP Request methods

We created the Hello World application using Rust, and one of the most amazing features of Rust is pattern matching. We will now leverage pattern matching to quickly respond to requests with HTTP methods other than GET and POST by sending an HTTP 405 (Method not allowed):

#[spin_component]
fn hello_world(req: Request) -> Result<Response> {
  match req.method() {
    &http::Method::GET => {
      handle_get(&req)
    },
    &http::Method::POST => {
      handle_post(&req)
    }
    _ => {
      Ok(http::Response::builder()
        .status(StatusCode::METHOD_NOT_ALLOWED)
        .body(None)?)
    }
  }
}

Handle GET requests

Let’s implement handle_get first. We want to pull <NAME> from the query string (if present) and construct a corresponding HTTP response. Instead of manually parsing the query string, we can use the existing querystring crate (crates.io/crates/querystring). Update Cargo.toml and add querystring as a dependency:

[package]
name = "hello-world"
authors = ["Thorsten Hans <[email protected]>"]
description = "Hello World - The Spin Edition"
version = "0.1.0"
edition = "2021"

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

[dependencies]
# Useful crate to handle errors.
anyhow = "1"
# Crate to simplify working with bytes.
bytes = "1"
# General-purpose crate with common HTTP types.
http = "0.2"
# The Spin SDK.
spin-sdk = { git = "https://github.com/fermyon/spin", tag = "v0.6.0" }
# Crate that generates Rust Wasm bindings from a WebAssembly interface.
wit-bindgen-rust = { git = "https://github.com/bytecodealliance/wit-bindgen", rev = "cb871cfa1ee460b51eb1d144b175b9aab9c50aba" }
querystring = "1.1"

[workspace]

With the dependencies in place, let’s implement handle_get:

fn handle_get(req: &Request) -> Result<Response> {
  const FALLBACK: &str = "Fermyon";
  const NAME_PARAM: &str = "name";

  let name = req.uri().query().map_or(FALLBACK, |qs| -> &str {
    let params = querystring::querify(qs);
    params
      .into_iter()
      .find(|p| p.0.to_lowercase() == NAME_PARAM)
      .map_or(FALLBACK, |p| p.1)
  });

  Ok(http::Response::builder()
    .status(http::StatusCode::OK)
    .header("Content-Type", "text")
    .body(Some(format!("Hello, {}", name).into()))?)
}

Handle POST requests

Next on our list is handling incoming POST requests with and grabbing the <NAME> from the actual JSON payload. Regarding JSON in Rust, there is only one real answer, serde (crates.io/crates/serde). That said, let’s add serde and serde_json as dependencies in Cargo.toml:

[package]
name = "hello-world"
authors = ["Thorsten Hans <[email protected]>"]
description = "Hello World - The Spin Edition"
version = "0.1.0"
edition = "2021"

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

[dependencies]
# Useful crate to handle errors.
anyhow = "1"
# Crate to simplify working with bytes.
bytes = "1"
# General-purpose crate with common HTTP types.
http = "0.2"
# The Spin SDK.
spin-sdk = { git = "https://github.com/fermyon/spin", tag = "v0.6.0" }
# Crate that generates Rust Wasm bindings from a WebAssembly interface.
wit-bindgen-rust = { git = "https://github.com/bytecodealliance/wit-bindgen", rev = "cb871cfa1ee460b51eb1d144b175b9aab9c50aba" }
querystring = "1.1"
serde = { features = ["derive" ], version = "1.0"} 
serde_json = "1"

Before we implement handle_post let’s create a simple struct representing the desired shape of our JSON payload:

use serde::Deserialize;

#[derive(Deserialize)]
pub struct HelloWorldRequestModel {
  pub name: String,
}

The HelloWorldPostModel is located in a dedicated file called models.rs that’s why the access to the type itself is qualified using models:: in the following handle_post implementation:

fn handle_post(req: &Request) -> Result<Response> {
  const FALLBACK: &str = "Fermyon";
  let body = req.body().clone().unwrap_or_default();
  let name = serde_json::from_slice::<models::HelloWorldRequestModel>(&body)
    .map(|p| p.name)
    .ok()
    .map(|n| {
      if n.len() == 0 {
        FALLBACK.to_string()
      } else {
        n.clone()
      }
    });

  match name {
    Some(n) => Ok(http::Response::builder()
      .status(http::StatusCode::OK)
      .header("Content-Type", "text")
      .body(Some(format!("Hello, {}", n).into()))?),
    None => Ok(http::Response::builder()
      .status(http::StatusCode::BAD_REQUEST)
      .header("Content-Type", "text")
      .body(Some(format!("Please provide a valid JSON payload").into()))?),
  }
}

Having all the requirements implemented, we can test our application again. Start the microservice using spin build --up, and you should again see spin printing essential information about it:

Finished release [optimized] target(s) in 0.04s
Successfully ran the build command for the Spin components.
Serving http://127.0.0.1:3000
Available Routes:
 hello-world: http://127.0.0.1:3000 (wildcard)

We can test if our microservice addresses all requirements by sending some slightly different requests to the endpoint:

curl http://localhost:3000\?name\=John
Hello, John

curl http://localhost:3000
Hello, Fermyon

curl -X POST --json '{ "name": "Jane"}' http://localhost:3000
Hello, Jane

curl -X POST --json '{ "name": ""}' http://localhost:3000
Hello, Fermyon

curl -iX POST --json '{ "firstName": "Mike"}' http://localhost:3000
HTTP/1.1 400 Bad Request
content-type: text
content-length: 35
date: Sun, 11 Dec 2022 13:53:41 GMT

Please provide a valid JSON payload

curl -iX DELETE http://localhost:3000
HTTP/1.1 405 Method Not Allowed
content-length: 0
date: Sun, 11 Dec 2022 13:54:11 GMT

Conclusion

WebAssembly on the server and in the cloud will change how we architect our cloud-native applications. Leveraging WebAssembly, we can run microservices with way higher density and address mission-critical concerns like security and isolation by default. With technologies like Spin, we can start the journey to Cloud-Native vNext today and prepare ourselves and our applications for the next big thing on the server side.

If you want to dive deeper into Spin and learn how to build more sophisticated applications, consult the official Spin Documentation and because it’s holiday season 🎄 you may want to participate and solve some challenges as part of the “Advent of Spin”.