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:
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 |
+------------------------------------------------------------------------+
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: /...
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:
-
Descriptionis a developer-facing description of the project. After the command is done, you’ll see it inspin.toml -
HTTP Pathis 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.tomlfile.
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
We can change directories to that newly created directory and start working. cd rust-spin-app.
-
Cargo.tomlis the Cargo configuration that most Rust projects use. -
spin.tomlis Spin’s configuration file -
src/lib.rsis 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]
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"]
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"
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"
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/foowould 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"]
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 newcorrectly calculated this for the location where the Rust toolchain will place the.wasmfile during acargo build -
allowed_outbound_hostsis 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.buildsection tellsspin buildwhat to do.- For a Rust project, it runs
cargo buildwith a few flags set. Using--releasestrips debugging symbols out of the Wasm binary, and makes thewasmfile much smaller and faster to load. -
watchspecifies which files thespin watchcommand should test for changes. Thespin watchcommand lets you code away whilespinauto-builds and keeps local copy running.
- For a Rust project, it runs
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")?)
}
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)
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
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
}
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")?)
}
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
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>")?)
}
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:

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)
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.
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)
Very well and clearly explained.