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:
-
Description
is a developer-facing description of the project. After the command is done, you’ll see it inspin.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
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]
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/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"]
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 acargo 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 tellsspin 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 thewasm
file much smaller and faster to load. -
watch
specifies which files thespin watch
command should test for changes. Thespin watch
command lets you code away whilespin
auto-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.