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:
- The latest version of Spin
- The NPM CLI
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
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
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
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"
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
}
}
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
}
}
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;
}
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
}
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:
- If the given path exists in storage:
- Get back that path's object (which is a
PageView
object) - Increment the
view
count - Store the updated record (which will overwrite the previous one)
- Return the view count
- Get back that path's object (which is a
- If the path does not exist:
- Create a new
PageView
- Save it in storage
- Return
1
(since we know this is the first page view)
- Create a new
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
}
}
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)
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).
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).
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)
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).
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)