DEV Community

Matt Butcher
Matt Butcher

Posted on

Storing state between serverless requests with TypeScript and Spin

One frequently cited frustration with using serverless or Functions as a Service (FaaS) is storing state between requests. This article shows how to use Spin's built-in key value store to store and retrieve JSON data between requests.

Spin is an open source tool for creating server-side WebAssembly apps.

If you are new to Spin, we'll cover most of the basics here. But you can also check out the official quickstart if you'd prefer. Spin supports other languages, including Rust, Python, and Go (and, of course, JavaScript). And the technique shown here works for those languages as well. But here we will focus on TypeScript.

To do this project, you will need:

For our project, we're going to create a very simple page view counter that will increment each time the page has been accessed.

Starting a New Project

To get started, let's create a new Spin TypeScript project named view-counter:

$ spin new http-ts view-counter --accept-defaults
Enter fullscreen mode Exit fullscreen mode

The above will create a new HTTP handler for TypeScript (thus http-ts) named view-counter, and we're just accepting the default configuration instead of walking through the wizard.

After this command, you should have a view-counter/ directory that looks like this:

$ tree view-counter/
view-counter/
├── README.md
├── package.json
├── spin.toml
├── src
│   └── index.ts
├── tsconfig.json
└── webpack.config.js

1 directory, 6 files
Enter fullscreen mode Exit fullscreen mode

We need to cd into the view-counter/ directory and initialize the project with npm:

$ cd view-counter
$ npm install

added 141 packages, and audited 142 packages in 6s

20 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
Enter fullscreen mode Exit fullscreen mode

Since we'll be using key value storage, we need to enable that feature for our app.

Enabling Key Value Storage

Before writing any code, we need to make one change to the spin.toml file. This is the main configuration file for Spin. Since WebAssembly has a capabilities-based security model, any time you want to enable a capability (key value storage), you need to tell Spin it is okay to allow the code to use that capability.

So in spin.toml, we'll add one line to the [[component]] declaration:

[[component]]
id = "view-counter"
source = "target/view-counter.wasm"
exclude_files = ["**/node_modules"]
key_value_stores = ["default"]   # <-- Enable default KV storage
[component.trigger]
route = "/..."
[component.build]
command = "npm run build"
Enter fullscreen mode Exit fullscreen mode

We just set key_value_stores = ["default"] to enable the default key value store.

With that, we are ready to code!

Editing the index.ts

Next up, we will be editing src/index.ts. When we ran spin new, it created the following for us:

import { HandleRequest, HttpRequest, HttpResponse} from "@fermyon/spin-sdk"

const encoder = new TextEncoder()

export const handleRequest: HandleRequest = async function(request: HttpRequest): Promise<HttpResponse> {
    return {
      status: 200,
        headers: { "foo": "bar" },
      body: encoder.encode("Hello from TS-SDK").buffer
    }
}
Enter fullscreen mode Exit fullscreen mode

The first thing we are going to do is simplify it by removing some unnecessary example code:

import { HandleRequest, HttpRequest, HttpResponse } from "@fermyon/spin-sdk"

export const handleRequest: HandleRequest = async function (request: HttpRequest): Promise<HttpResponse> {
  let body = "Hello Dev.to World!"
  return {
    status: 200,
    body: body
  }
}
Enter fullscreen mode Exit fullscreen mode

Now our function just returns a simple response. Next up, let's create a simple class that represents a page view.

class PageView {
  path: string = "";
  count: number = 0;
}
Enter fullscreen mode Exit fullscreen mode

This class isn't particularly sophisticated, but it will serve our purpose of showing how to store JSON data in key value storage.

Now we can write a function that stores and retrieves the record.

import { Kv, HandleRequest, HttpRequest, HttpResponse } from "@fermyon/spin-sdk"

// snip

const logRequest = async function (path: string): Promise<number> {
  let store = Kv.openDefault()

  // If there is already a record for this path, updated it and re-store it.
  if (store.exists(path)) {
    let view = store.getJson(path)
    view.count++
    store.setJson(path, view)
    return view.count;
  }

  // Otherwise, create a new record for this path.
  let new_view: PageView = {
    path: path,
    count: 1
  }
  store.setJson(path, new_view)
  return 1
}
Enter fullscreen mode Exit fullscreen mode

Note that we added Kv to the import line now. That's the main key value store object.

First, we open the default key value store with Kv.openDefault(). This gives us a handle to the key value storage system. We'll use three functions:

  • exists() to see if a key exists
  • getJson() to get a JSON object by name
  • setJson() to store a name and JSON object as a pair

The flow of the code goes like this:

  1. If the given path exists in storage:
    1. Get back that path's object (which is a PageView object)
    2. Increment the view count
    3. Store the updated record (which will overwrite the previous one)
    4. Return the view count
  2. If the path does not exist:
    1. Create a new PageView
    2. Save it in storage
    3. Return 1 (since we know this is the first page view)

Now we can update our handleRequest() function to call logRequest():

export const handleRequest: HandleRequest = async function (request: HttpRequest): Promise<HttpResponse> {
  let count = await logRequest(request.uri)
  let body = `The path ${request.uri} has been accessed ${count} time(s).`
  return {
    status: 200,
    body: body
  }
}

Enter fullscreen mode Exit fullscreen mode

In this updated code, we've just added the line let count = await logRequest(request.uri) and then updated the body variable to have the path and the count.

We can locally build and run our app in one command:

$ spin build --up
Building component view-counter with `npm run build`

> view-counter@1.0.0 build
> npx webpack --mode=production && mkdir -p target && spin js2wasm -o target/view-counter.wasm dist/spin.js

asset spin.js 13.3 KiB [emitted] (name: main)
orphan modules 5.91 KiB [orphan] 11 modules
runtime modules 937 bytes 4 modules
cacheable modules 10.2 KiB
  ./src/index.ts + 5 modules 9.61 KiB [built] [code generated]
  ./node_modules/typedarray-to-buffer/index.js 646 bytes [built] [code generated]
webpack 5.89.0 compiled successfully in 689 ms

Starting to build Spin compatible module
Preinitiating using Wizer
Optimizing wasm binary using wasm-opt
Spin compatible module built successfully
Finished building all Spin components
Logging component stdio to ".spin/logs/"
Storing default key-value data to ".spin/sqlite_key_value.db"

Serving http://127.0.0.1:3000
Available Routes:
  view-counter: http://127.0.0.1:3000 (wildcard)
Enter fullscreen mode Exit fullscreen mode

We could test in a Web browser or just use curl:

$ curl localhost:3000/            
The path / has been accessed 1 time(s).
$ curl localhost:3000/            
The path / has been accessed 2 time(s).
Enter fullscreen mode Exit fullscreen mode

Now, here's a fun side-effect of the way we built our app. Since we left the default settings when we did a spin new, our app will answer on any path under /. So we can make a request to another path, and the same function will be called. But since we're storing the hit counter by path, we should see it go back to 1:

$ curl localhost:3000/hello       
The path /hello has been accessed 1 time(s).
Enter fullscreen mode Exit fullscreen mode

Effectively, we now have two keys stored in key value storage: / and /hello. Each has its own counter.

Deploying to the Cloud

This part is optional, but shows you how key value storage works in a prod-like environment.

Deploying to Fermyon Cloud requires only a GitHub OAuth handshake (which I already did prior to this example using spin login). From there, we can use a simple command, spin deploy to push our app to the cloud:

$ spin deploy    
Uploading view-counter version 0.1.0-rc8562996 to Fermyon Cloud...
Deploying...
Waiting for application to become ready.......... ready
Available Routes:
  view-counter: https://view-counter-djfdh6p3.fermyon.app (wildcard)
Enter fullscreen mode Exit fullscreen mode

Now we have a public URL that anyone can hit.

$ curl https://view-counter-djfdh6p3.fermyon.app                     
The path / has been accessed 1 time(s).
Enter fullscreen mode Exit fullscreen mode

Note that the counter reset to 1. That's because when we deployed our app, Fermyon automatically provisioned us an in-cloud key value store. I now have one storage copy locally, and one in the cloud. And the one in the cloud is production-grade.

Wrap-up

Saving state in Spin applications is easy using the built-in key value store. Here we built a simple page counter that stored JSON objects for each request path. And we did all of this without needing to install or manage another external service. It's just part of Spin.

To learn more about key value storage, the best place to check is the official documentation. And to get some ideas of what you can build with Spin, check out the Spin Up Hub. And if you find any bugs or want to hack on the Spin source code, it's all on GitHub.

Top comments (0)