Building Spin components in Rust

Spin aims to have best-in-class support for building components in Rust, and writing such components should be familiar for Rust developers.

This guide assumes you have Spin installed. If this is your first encounter with Spin, please see the Quick Start, which includes information about installing Spin with the Rust templates, installing required tools, and creating Rust applications.

This guide assumes you are familiar with the Rust programming language, but if you are just getting started, be sure to check the official resources for learning Rust.

All examples from this page can be found in the Spin Rust SDK repository on GitHub.

Want to go straight to the Spin SDK reference documentation? Find it here.

Prerequisites

Install the Templates

You don’t need the Spin Rust templates to work on Rust components, but they speed up creating new applications and components. You can install them as follows:

$ spin templates install --git https://github.com/spinframework/spin --update
Copying remote template source
Installing template redis-rust...
Installing template http-rust...
... other templates omitted ...
+------------------------------------------------------------------------+
| Name                Description                                        |
+========================================================================+
| ... other templates omitted ...                                        |
| http-rust           HTTP request handler using Rust                    |
| redis-rust          Redis message handler using Rust                   |
| ... other templates omitted ...                                        |
+------------------------------------------------------------------------+

Note: The Rust templates are in a repo that contains several other languages; they will all be installed together.

Install the Tools

To build Spin components, you’ll need the wasm32-wasip2 target for Rust.

$ rustup target add wasm32-wasip2

If you don’t have the target installed, then when you try to build a Spin component, you’ll see an error similar to this:

error[E0463]: can't find crate for `core`
  |
  = note: the `wasm32-wasip2` target may not be installed
  = help: consider downloading the target with `rustup target add wasm32-wasip2`

For more information about this error, try `rustc --explain E0463`.

Check that you have the WASI target installed by running rustup target list --installed, or follow the instructions in the message and run rustup target add wasm32-wasip2. This is the most common source of problems when starting out with Rust in Spin!

HTTP Service Components

In Spin, HTTP service components are triggered by the occurrence of an HTTP request, and must return an HTTP response at the end of their execution. Components can be built in any language that compiles to WASI, but Rust has improved support for writing Spin components with the Spin Rust SDK.

Make sure to read the page describing the HTTP trigger for more details about building HTTP applications.

Building a Spin HTTP component using the Rust SDK means writing a single function decorated with the #[http_service] attribute. The function must be async. It takes an HTTP request as its only parameter, and returns an HTTP response. (More precisely, the parameter can be anything that implements the FromRequest trait, and the result can be anything that implements the IntoResponse trait.) Here’s a typical simple example:

use spin_sdk::http::{Request, Response, IntoResponse};
use spin_sdk::http_service;

/// A simple Spin HTTP component.
#[http_service]
async fn handle_hello_rust(_req: Request) -> anyhow::Result<impl IntoResponse> {
    Ok(Response::builder()
        .status(200)
        .header("content-type", "text/plain")
        .body("Hello, World!".to_string())?)
}

The important things to note in the implementation above:

  • the spin_sdk::http_service macro marks the function as the entry point for the Spin component
  • the function signature — async fn hello_world(req: Request) -> Result<impl IntoResponse> — the Spin HTTP component allows for a flexible set of response types via the IntoResponse trait, including the SDK’s Response type and the Response type from the Rust http crate. See the section on using the http crate for more information.

The spin_sdk::http types are re-exports from the http crate. You can use them with other Rust crates that work with http, and related ecosystem crates such as http_body and http_body_util. We’ll show an example of working with the popular Axum framework below.

Redis Subscriber Components

Besides the HTTP trigger, Spin has built-in support for a Redis trigger — which will connect to a Redis instance and will execute Spin components for new messages on the configured channels.

See the Redis trigger for details about the Redis trigger.

Writing a Redis component in Rust also takes advantage of the SDK:

use anyhow::Result;
use spin_sdk::redis_subscriber;

/// A simple Spin Redis component.
#[redis_subscriber]
async fn on_message(message: Vec<u8>) -> Result<()> {
    println!("{}", std::str::from_utf8(&message)?);
    Ok(())
}
  • the spin_sdk::redis_subscriber macro marks the function as the entry point for the Spin component
  • in the function signature — async fn on_message(msg: Vec<u8>) -> anyhow::Result<()>msg contains the payload from the Redis channel
  • the component returns a Rust anyhow::Result, so if there is an error processing the request, it returns an anyhow::Error.

The component can be built with Cargo by executing:

$ cargo build --target wasm32-wasip2 --release

The manifest for a Redis application must contain the address of the Redis instance the trigger must connect to:

spin_manifest_version = 2
name = "spin-redis"
version = "0.1.0"

[application.trigger.redis]
address = "redis://localhost:6379"

[[trigger.redis]]
channel = "messages"
component = { source = "target/wasm32-wasip2/release/spinredis.wasm" }

This application will connect to redis://localhost:6379, and for every new message on the messages channel, the echo-message component will be executed:

# first, start redis-server on the default port 6379
$ redis-server --port 6379
# then, start the Spin application
$ spin up
# the application log file will output the following
INFO spin_redis_engine: Connecting to Redis server at redis://localhost:6379
INFO spin_redis_engine: Subscribed component 0 (echo-message) to channel: messages

For every new message on the messages channel:

$ redis-cli
127.0.0.1:6379> publish messages "Hello, there!"

Spin will instantiate and execute the component we just built, which will emit the println! message to the application log file:

INFO spin_redis_engine: Received message on channel "messages"
Hello, there!

You can find a complete example for a Redis-triggered Rust component in the Spin repository on GitHub.

Sending Outbound HTTP Requests

If allowed, Spin components can send outbound HTTP requests. Let’s see an example of a component that makes a request to an API that returns random animal facts and inserts a custom header into the response before returning:

use anyhow::Result;
use spin_sdk::{
    http::{EmptyBody, IntoResponse, Request, Method, Response},
    http_service,
};

#[http_service]
async fn send_outbound(_req: Request) -> Result<impl IntoResponse> {
    // Create the outbound request object
    let req = Request::builder()
        .method(Method::Get)
        .uri("https://random-data-api.fermyon.app/animals/json")
        .body(EmptyBody::new())?;

    // Send the request and await the response
    let res: Response = spin_sdk::http::send(req).await?;

    println!("Random data status code {}", res.status());  // log the response status

    // Pass the upstream response back to the client
    Ok(res)
}

Before we can execute this component, we need to add the random-data-api.fermyon.app domain to the component’s allowed_outbound_hosts list in the application manifest. This contains the list of domains the component is allowed to make network requests to:

# spin.toml
spin_manifest_version = 2

[application]
name = "animal-facts"
version = "1.0.0"

[[trigger.http]]
route = "/..."
component = "get-animal-fact"

[component.get-animal-fact]
source = "get-animal-fact/target/wasm32-wasip2/release/get_animal_fact.wasm"
allowed_outbound_hosts = ["https://random-data-api.fermyon.app"]

Running the application using spin up will start the HTTP listener locally (by default on localhost:3000), and our component can now receive requests in route /outbound:

$ curl -i localhost:3000
HTTP/1.1 200 OK
date: Fri, 27 Oct 2023 03:54:36 GMT
content-type: application/json; charset=utf-8
content-length: 185
spin-component: get-animal-fact

{"timestamp":1684299253331,"fact":"Reindeer grow new antlers every year"}   

Without the allowed_outbound_hosts field populated properly in spin.toml, the component would not be allowed to send HTTP requests, and sending the request would result in a “Destination not allowed” error.

You can set allowed_outbound_hosts = ["https://*:*"] if you want to allow the component to make requests to any HTTP host. This is not recommended unless you have a specific need to contact arbitrary servers and perform your own safety checks.

We just built a WebAssembly component that sends an HTTP request to another service, manipulates that result, then responds to the original request. This can be the basis for building components that communicate with external databases or storage accounts, or even more specialized components like HTTP proxies or URL shorteners.

The Spin SDK for Rust provides more flexibility than we show here, including allowing streaming uploads or downloads. See the Outbound HTTP API Guide for more information.

Routing in a Component

You can route within a Rust HTTP component using your favourite Rust HTTP routing library.

Earlier versions of the Spin SDK included a their own Router, which was able to work with WASI/Spin HTTP types. From Spin SDK 6, the SDK uses the ecosystem-standard http types, and so you can ecosystem routers without changes.

This example uses the Axum router in a Spin component:

use axum::{
    routing::{any, get},
    Router,
};
use spin_sdk::http::{IntoResponse, Request, Response};
use spin_sdk::http_service;
use tower_service::Service;

/// Demonstrates integration with the Axum web framework
#[http_service]
async fn handler(req: Request) -> impl IntoResponse {
    Router::new()
        .route("/{*all}", any(api::echo_wildcard))
        .route("/goodbye/{planet}", get(api::goodbye_planet))
        .call(req)
        .await
}

mod api {
    use super::*;
    use axum::extract::Path;

    // /goodbye/:planet
    pub async fn goodbye_planet(Path(planet): Path<String>) -> impl axum::response::IntoResponse {
        Response::new(planet)
    }

    // /*
    pub async fn echo_wildcard(Path(all): Path<String>) -> impl axum::response::IntoResponse {
        Response::new(all)
    }
}

Storing Data in Redis From Rust Components

Using the Spin Rust SDK, you can use the Redis key/value store, and publish messages to Redis channels. This can be used from both HTTP and Redis triggered components.

Let’s see how we can use the Rust SDK to connect to Redis:

use anyhow::Context;
use spin_sdk::{
    http::{responses::internal_server_error, EmptyBody, IntoResponse, Request, Response},
    http_service,
    redis::Connection,
};

// The environment variable set in `spin.toml` that points to the
// address of the Redis server that the component will publish
// a message to.
const REDIS_ADDRESS_ENV: &str = "REDIS_ADDRESS";

// The environment variable set in `spin.toml` that specifies
// the Redis channel that the component will publish to.
const REDIS_CHANNEL_ENV: &str = "REDIS_CHANNEL";

/// This HTTP component demonstrates fetching a value from Redis
/// by key, setting a key with a value, and publishing a message
/// to a Redis channel. The component is triggered by an HTTP
/// request served on the route configured in the `spin.toml`.
#[http_service]
async fn publish(_req: Request) -> anyhow::Result<impl IntoResponse> {
    let address = std::env::var(REDIS_ADDRESS_ENV)?;
    let channel = std::env::var(REDIS_CHANNEL_ENV)?;

    // Establish a connection to Redis
    let conn = Connection::open(&address).await?;

    // Get the message to publish from the Redis key "mykey"
    let payload = conn
        .get("mykey")
        .await
        .context("Error querying Redis")?
        .context("'mykey' was unexpectedly empty")?;

    // Set the Redis key "spin-example" to value "Eureka!"
    conn.set("spin-example", &"Eureka!".to_owned().into_bytes())
        .await
        .context("Error executing Redis set command")?;

    // Set the Redis key "int-key" to value 0
    conn.set("int-key", &format!("{:x}", 0).into_bytes())
        .await
        .context("Error executing Redis set command")?;
    let int_value = conn
        .incr("int-key")
        .await
        .context("Error executing Redis incr command")?;
    assert_eq!(int_value, 1);

    // Publish to Redis
    match conn.publish(&channel, &payload).await {
        Ok(()) => Ok(Response::new(EmptyBody::new())),
        Err(_e) => Ok(internal_server_error())
    }
}

As with all networking APIs, you must grant access to Redis hosts via the allowed_outbound_hosts field in the application manifest:

[component.redis-test]
environment = { REDIS_ADDRESS = "redis://127.0.0.1:6379", REDIS_CHANNEL = "messages" }
# Note this contains only the host and port - do not include the URL!
allowed_outbound_hosts = ["redis://127.0.0.1:6379"]

This HTTP component can be paired with a Redis component, triggered on new messages on the messages Redis channel.

You can find a complete example for using outbound Redis from an HTTP component in the Spin repository on GitHub.

Async and Streaming Idioms in Rust

When a Spin API returns a potentially large number of values, such as database query APIs, the convention is to return the values as a stream, plus a future containing the result of the operation. For example, the key-value Store::get_keys function returns (StreamReader<String>, impl Future<Output = Result<(), Error>>). This signature is likely to be unfamiliar. The way to read it is:

  • Spin will stream values to you until either there are no more values, or an error occurs.
  • When that happens, you must await the future to find out which one it was.

For example, here’s how you might use Store::get_keys:

let (keys, result) = store.get_keys().await;
while let Some(key) = key.next().await {
    // do something with `key`
}
result.await?; // check if the key stream hit an error

The future does not resolve until the stream ends, so be sure not to await it until you’ve finished with the stream.

Spawning Asynchronous Tasks

You can spawn an asynchronous task in a component using the spin_sdk::wasip3::spawn() function, passing it a Future<Output = ()>. The future is then run to completion in the background. The task may outlive the entry point of your component - this is crucial in, for example, the HTTP trigger, where your handler function doesn’t necessarily want to wait for all response data to be available before it starts sending.

Here’s a small example.

use bytes::Bytes;
use futures::{SinkExt, StreamExt};
use http_body_util::StreamBody;
use spin_sdk::http::{IntoResponse, Request, Response};
use spin_sdk::http_service;

#[http_service]
async fn handle(_req: Request) -> anyhow::Result<impl IntoResponse> {
    let store = spin_sdk::key_value::Store::open_default().await?;

    // Create a Rust channel for the async task to communicate over.
    let (mut tx, rx) = futures::channel::mpsc::channel::<Bytes>(1024);

    // We will use the read end of the channel as the HTTP body. This
    // involves some type adaptation, but again is normal Rust.
    let rx = rx.map(|value| anyhow::Ok(http_body::Frame::data(value)));
    let response = Response::new(StreamBody::new(rx));

    // Spawn a WebAssmbly task. This is going to run in the background,
    // even after the `handle` function has returned.
    spin_sdk::wasip3::spawn(async move {
        // Fetch the first part of the data...
        if let Ok(Some(greeting)) = store.get("greeting").await {
            // ...and send it over the channel into the response...
            tx.send(greeting.into()).await.unwrap();
        };
        // ...and now the second part of the data.
        if let Ok(Some(who_to_greet)) = store.get("greetee").await {
            tx.send(" ".into()).await.unwrap();
            tx.send(who_to_greet.into()).await.unwrap();
        };
        // When the async block exits, Rust drops `tx`. This marks
        // the end of the body stream, and Spin will end the response.
    });

    // Return the response to Spin. Spin starts streaming the response
    // immediately. The client will see response data as it becomes available
    // rather than having to wait while Spin gathers the entire response.
    Ok(response)
}

Creating Futures and Streams

The Rust SDK wasip3 module provides functions for creating Wasm Component Model futures and streams. These are low-level primitives that you will usually interact with only when bypassing Rust SDK wrappers to use raw WIT bindings.

Use spin_sdk::wasip3::wit_future::new() to create a future.

Use spin_sdk::wasip3::wit_stream::new() to create a stream. This returns a writer and a reader. The writer is typically handed to a spin_sdk::wasip3::spawn task to asynchronously send values into the stream. The reader is typically passed to an API that takes a stream parameter, for example acting as the body in an HTTP response.

You can use these functions only for types which appear in (respectively) futures or streams in a Spin or WASI API. (They’d be useless for other types anyway.) If you get a compilation error saying the type does not implement FuturePayload or StreamPayload, check the API where you intend to use the future or stream - you have a type mismatch somewhere! (These traits are generated for the types that need them - you can’t implement them yourself.)

Storing Data in the Spin Key-Value Store

Spin has a key-value store built in. For information about using it from Rust, see the key-value API guide.

Serializing Objects to the Key-Value Store

The Spin key-value API stores and retrieves only lists of bytes. The Rust SDK provides helper functions that allow you to store and retrieve Serde serializable values in a typed way. The underlying storage format is JSON (and is accessed via the get_json and set_json helpers).

To make your objects serializable, you will also need a reference to serde. The relevant Cargo.toml entries look like this:

[dependencies]
// --snip --
serde = { version = "1", features = ["derive"] }
// --snip --

We configure our application to provision the default key_value_stores by adding the following line to our application’s manifest (the spin.toml file), at the component level:

[component.json-test]
key_value_stores = ["default"]

The Rust code below shows how to store and retrieve serializable objects from the key-value store (note how the example below implements Serde’s derive feature):

use anyhow::Context;
use serde::{Deserialize, Serialize};
use spin_sdk::{
    http::{IntoResponse, Request, Response},
    http_service,
    key_value::Store,
};

// Define a serializable User type
#[derive(Serialize, Deserialize)]
struct User {
    fingerprint: String,
    location: String,
}

#[http_service]
async fn handle_request(_req: Request) -> anyhow::Result<impl IntoResponse> {
    // Open the default key-value store
    let store = Store::open_default().await?;

    // Create an instance of a User object and populate the values
    let user = User {
        fingerprint: "0x1234".to_owned(),
        location: "Brisbane".to_owned(),
    };
    // Store the User object using the "my_json" key
    store.set_json("my_json", &user).await?;
    // Retrieve the user object from the key-value store, using the "my_json" key
    let retrieved_user: User = store.get_json("my_json").await?.context("user not found")?;
    // Return the user's fingerprint as the response body
    Ok(Response::new(retrieved_user.fingerprint))
}

Once built and running (using spin up --build) you can test the above example in your browser (by visiting localhost:3000) or via curl, as shown below:

$ curl localhost:3000
HTTP/1.1 200 OK

0x1234

For more information on the Rust key-value API see the Spin SDK documentation.

Storing Data in SQLite

For more information about using SQLite from Rust, see SQLite storage.

Storing Data in Relational Databases

Spin provides clients for MySQL and PostgreSQL. For information about using them from Rust, see Relational Databases.

Using External Crates in Rust Components

In Rust, Spin components are regular libraries that contain a function annotated using the http_service macro, compiled to the wasm32-wasip2 target. This means that any crate that compiles to wasm32-wasip2 can be used when implementing the component.

AI Inferencing From Rust Components

For more information about using Serverless AI from Rust, see the Serverless AI API guide.

Troubleshooting

If you bump into issues building and running your Rust component, here are some common causes of problems:

  • Make sure cargo is present in your path.
  • Make sure the Rust version is recent.

Rust Version

  • To check: run cargo --version.
  • To update: run rustup update.
  • Make sure the wasm32-wasip2 compiler target is installed.
    • To check: run rustup target list --installed and check that wasm32-wasip2 is on the list.
    • To install: run rustup target add wasm32-wasip2.
  • Make sure you are building in release mode. Spin manifests refer to your Wasm file by a path, and the default path corresponds to release builds.
    • To build manually: run cargo build --release --target wasm32-wasip2.
    • If you’re using spin build and the templates, this should be set up correctly for you.
  • Make sure that the source field in the component manifest match the path and name of the Wasm file in target/wasm32-wasip2/release. These could get out of sync if you renamed the Rust package in its Cargo.toml.

Manually Creating New Projects With Cargo

The recommended way of creating new Spin projects is by starting from a template. This section shows how to manually create a new project with Cargo.

When creating a new Spin project with Cargo, you should use the --lib flag:

$ cargo init --lib

A Cargo.toml with standard Spin dependencies looks like this:

[package]
name = "your-app"
version = "0.1.0"
edition = "2021"

[lib]
# Required to have a `cdylib` (dynamic library) to produce a Wasm module.
crate-type = [ "cdylib" ]

[dependencies]
# Useful crate to handle errors.
anyhow = "1"
# The Spin SDK.
spin-sdk = "6.0"

Read the Rust Spin SDK Documentation

Although you learned a lot by following the concepts and samples shown here, you can dive even deeper and read the Rust Spin SDK documentation.