Spin Key-Value Store

Key Value Store With Spin Applications

Spin applications are best suited for event-driven, stateless workloads that have low-latency requirements. Keeping track of the application’s state (storing information) is an integral part of any useful product or service. For example, users (and the business) will expect to store and load data/information at all times during an application’s execution. Spin has support for applications that need data in the form of key/value pairs and are satisfied by a Basically Available, Soft State, and Eventually Consistent (BASE) model. Workload examples include general value caching, session caching, counters, and serialized application state. In this tutorial, you will learn how to do the following:

  • Create a Spin application with spin new
  • Use the key value store SDK to get, set, and list key value pairs
  • Configure your application manifest (spin.toml) to use the default key value store
  • Run your key value store Spin application locally with spin up

Tutorial Prerequisites

First, follow this guide to install Spin. To ensure you have the correct version, you can check with this command:

$ spin --version

Please ensure you’re on Spin version 2.0 or newer.

Python

If you are planning on using Python for this tutorial, please ensure that you have Python 3.10 or later installed on your system. You can check your Python version by running:

python3 --version

If you do not have Python 3.10 or later, you can install it by following the instructions here.

Creating a New Spin Application

Let’s create a Spin application that will send and retrieve data from a key value store. To make things easy, we’ll start from a template using the following commands (learn more):

$ spin new -t http-rust spin-key-value

# Reference: https://github.com/fermyon/spin-rust-sdk/tree/stable/examples/rust-key-value
$ spin new -t http-ts spin-key-value

# Reference: https://github.com/fermyon/spin-js-sdk/tree/main/examples/spin-host-apis/spin-kv
$ spin new -t http-py spin-key-value

# Reference: https://github.com/fermyon/spin-python-sdk/tree/main/examples/spin-kv
$ spin new -t http-go spin-key-value

# Reference: https://github.com/fermyon/spin-go-sdk/tree/stable/examples/key-value

Configuration

Good news - Spin will take care of setting up your Key Value store. However, in order to make sure your Spin application has permission to access the Key Value store, you must add the key_value_stores = ["default"] line in the [component.<component-name>] section of the spin.toml file, for each component which needs access to the Key Value store. This line is necessary to communicate to Spin that a given component has access to the default Key Value store. A newly scaffolded Spin application will not have this line; you will need to add it.

Note: [component.spin_key_value] contains the name of the component. If you used a different name, when creating the application, this sections name would be different.

[component.spin_key_value]
...
key_value_stores = ["default"]
...

Tip: You can choose between various store implementations by modifying the runtime configuration. The default implementation uses SQLite within the Spin framework.

Each Spin application’s key_value_stores instances are implemented on a per-component basis across the entire Spin application. This means that within a multi-component Spin application (which has the same key_value_stores = ["default"] configuration line), each component will access that same data store. If one of your application’s components creates a new key/value pair, another one of your application’s components can update/overwrite that initial key/value after the fact.

The Spin TOML File

We will give our components access to the key value store by adding the key_value_stores = ["default"] in the `[component.] section as shown below:

spin_manifest_version = 2

[application]
name = "spin-key-value"
version = "0.1.0"
authors = ["Your Name <your-name@example.com>"]
description = "A simple application that exercises key-value storage."

[[trigger.http]]
route = "/..."
component = "spin-key-value"

[component.spin-key-value]
...
key_value_stores = ["default"]
...

Write Code to Save and Load Data

In this section, we use the Spin SDK to open and persist our application’s data inside our default key/value store. This is a special store that every environment running Spin applications will make available for their application.

The Spin SDK Version

If you have an existing application and would like to try out the key/value feature, please check the Spin SDK reference in your existing application’s configuration. It is highly recommended to upgrade Spin and the SDK versions to the latest version available.

Source Code

Now let’s use the Spin SDK to:

  • add new data
  • check that the new data exists
  • retrieve that data
  • delete data
  • check the data has been removed
use spin_sdk::{
    http::{IntoResponse, Request, Response, Method},
    http_component,
    key_value::Store,
};

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

    let (status, body) = match *req.method() {
        Method::Post => {
            // Add the request (URI, body) tuple to the store
            store.set(req.path(), req.body())?;
            println!(
                "Storing value in the KV store with {:?} as the key",
                req.path()
            );
            (200, None)
        }
        Method::Get => {
            // Get the value associated with the request URI, or return a 404 if it's not present
            match store.get(req.path())? {
                Some(value) => {
                    println!("Found value for the key {:?}", req.path());
                    (200, Some(value))
                }
                None => {
                    println!("No value found for the key {:?}", req.path());
                    (404, None)
                }
            }
        }
        Method::Delete => {
            // Delete the value associated with the request URI, if present
            store.delete(req.path())?;
            println!("Delete key {:?}", req.path());
            (200, None)
        }
        Method::Head => {
            // Like GET, except do not return the value
            let code = if store.exists(req.path())? {
                println!("{:?} key found", req.path());
                200
            } else {
                println!("{:?} key not found", req.path());
                404
            };
            (code, None)
        }
        // No other methods are currently supported
        _ => (405, None),
    };
    Ok(Response::new(status, body))
}
import { ResponseBuilder, Kv } from "@fermyon/spin-sdk";

const encoder = new TextEncoder()
const decoder = new TextDecoder()

export async function handler(req: Request, res: ResponseBuilder) {

  let store = Kv.openDefault()
  let status = 200
  let body

  switch (req.method) {
    case "POST":
      store.set(req.uri, await req.text() || (new Uint8Array()).buffer)
      console.log(`Storing value in the KV store with ${req.uri} as the key`);
      break;
    case "GET":
      let val
      try {
        val = store.get(req.uri)
        body = decoder.decode(val)
      	console.log(`Found value for the key ${req.uri}`);
      } catch (error) {
      	console.log(`Key ${req.uri} not found`);
        status = 404
      }
      break;
    case "DELETE":
      store.delete(req.uri)
      console.log(`Deleted Key ${req.uri}`);
      break;
    case "HEAD":
      if (!store.exists(req.uri)) {
        console.log(`Key ${req.uri} not found`);
        status = 404
      } else {
        console.log(`Found Key ${req.uri}`);
      }
      break;
    default:
  }
  res.status(status)
  res.send(body)
}
from spin_sdk import http, key_value
from spin_sdk.http import Request, Response

class IncomingHandler(http.IncomingHandler):
    def handle_request(self, request: Request) -> Response:
        with key_value.open_default() as store:
            match request.method:
                case "GET":
                    value = store.get(request.uri)
                    if value:
                        status = 200
                        print(f"Found key {request.uri}")
                    else:
                        status = 404
                        print(f"Key {request.uri} not found")
                    return Response( status, {"content-type": "text/plain"}, value)
                case "POST":
                    store.set(request.uri, request.body)
                    print(f"Stored key {request.uri}")
                    return Response(200, {"content-type": "text/plain"})
                case "DELETE":
                    store.delete(request.uri)
                    print(f"Deleted key {request.uri}")
                    return Response(200, {"content-type": "text/plain"})
                case "HEAD":
                    if store.exists(request.uri):
                        print(f"Found key {request.uri}")
                        return Response(200, {"content-type": "text/plain"})
                    print(f"Key not found {request.uri}")
                    return Response(404, {"content-type": "text/plain"})
                case default:
                    return Response(405, {"content-type": "text/plain"})

</div>


<div class="multitab-content" data-title="TinyGo">

```go
package main

import (
	"io"
	"net/http"
	"fmt"

	spin_http "github.com/fermyon/spin/sdk/go/v2/http"
	"github.com/fermyon/spin/sdk/go/v2/kv"
)

func init() {
	// handler for the http trigger
	spin_http.Handle(func(w http.ResponseWriter, r *http.Request) {
		store, err := kv.OpenStore("default")
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		defer store.Close()

		body, err := io.ReadAll(r.Body)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}

		switch r.Method {
		case http.MethodPost:
			err := store.Set(r.URL.Path, body)
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}
			fmt.Println("Stored the key at:", r.URL.Path)
			w.WriteHeader(http.StatusOK)
		case http.MethodGet:
			value, err := store.Get(r.URL.Path)
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}

			fmt.Println("Got the key:", r.URL.Path)
			w.WriteHeader(http.StatusOK)
			w.Write(value)
		case http.MethodDelete:
			err := store.Delete(r.URL.Path)
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}
			fmt.Println("Deleted the key:", r.URL.Path)
			w.WriteHeader(http.StatusOK)
		case http.MethodHead:
			exists, err := store.Exists(r.URL.Path)
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}

			if exists {
				w.WriteHeader(http.StatusOK)
				fmt.Println("Found key:", r.URL.Path)
				return
			}

			fmt.Println("Didn't find the key:", r.URL.Path)
			w.WriteHeader(http.StatusNotFound)
		default:
			http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
		}
	})
}

func main() {}

Building and Running Your Spin Application

Now, let’s build and run our Spin Application locally. Run the following command to build your application:

$ spin build
$ spin up

If you ever receive the error Handler returned an error: Error::AccessDenied, please make sure you’ve included a list of allowed key_value_stores in your spin.toml file (as shown above in the configuration section).

Storing and Retrieving Data From Your Default Key/Value Store

Once you have completed this minimal configuration and deployed your application, data will be persisted across requests. Let’s begin by creating a POST request that stores a JSON key/value object:

# Create a new POST request and set the key/value pair of foo:bar
$ curl localhost:3000/test -H 'Content-Type: application/json' -d '{"foo":"bar"}'

We can now use a HEAD request to confirm that our component is holding data for us. Essentially, all we want to see here is a 200 OK response when calling our components endpoint (/test). Let’s give it a try:

$ curl -I localhost:3000/test

HTTP/1.1 200 OK

Perfect, 200 OK. Now, let’s create a GET request that fetches the data from our component:

# Create a GET request and fetch the key/value that we stored in the previous request
$ curl localhost:3000/test

{"foo": "bar"}

Great! The above command successfully returned our data as intended.

Lastly, we show how to create a DELETE request that removes the data for this specific component altogether:

$ curl -X DELETE localhost:3000/test

Note how all of the above commands returned 200 OK responses. In these examples, we were able to POST, HEAD (check to see if data exists), GET and also DELETE data from our component.

Interestingly there is one more request we can re-run before wrapping up this tutorial. If no data exists in the component’s endpoint of /test (which is technically the case now that we have sent the DELETE request) the HEAD request should correctly return 404 Not Found. You can consider this a type of litmus test; let’s try it out:

$ curl -I localhost:3000/test

HTTP/1.1 404 Not Found

As we can see above, there is currently no data found at the /test endpoint of our application.

(Optional) Deploy Your App To Fermyon Cloud

Optionally, if you’d like to deploy your application and key value store to Fermyon Cloud here are the required steps.

First, login to your Fermyon Cloud account. If you do not have one already, this will take you through the signup process for a free account.

$ spin login

Copy your one-time code:

XXXXXXXX

...and open the authorization page in your browser:

https://cloud.fermyon.com/device-authorization

Waiting for device authorization...
Device authorized!

Now that we have our dependencies squared away, let’s deploy our application to Fermyon Cloud. From your application root folder, run the following command and opt to allow Fermyon Cloud to create a key value store on your behalf:

$ spin cloud deploy
spin deploy
Uploading spin-key-value version 0.1.0-r234fe5a4 to Fermyon Cloud...
Deploying...
App "spin-key-value" accesses a key value store labeled "default"
Would you like to link an existing key value store or create a new key value store?:
> Use an existing key value store and link app to it
  Create a new key value store and link the app to it

If you’re interested in learning more about how to link your Spin app to different key value store instances on Fermyon Cloud, check out our Key Value Links and Labels tutorial.

Congratulations, you have a Spin application and associated Key Value store running up in Fermyon Cloud! You can visit it by clicking on the Spin application’s domain name generated in the CLI output, which has the following pattern: spin-key-value-<RANDOM>.fermyon.app.

Next Steps