The nice thing about writing a serverless functions is that you only have to write an event handler. Much of the boilerplate code you'd write for a regular webserver-style app can be omitted.
In this post, we'll write a server-side Javascript function and then build it into a WebAssembly binary using the open source Spin tool. Our code will be less than a dozen lines long in total, so this is a concise introduction to WebAssembly and serverless functions that won't require you to spend a lot of time figuring out a code sample.
To get started, we need to install Spin locally. Spin is a tool for building WebAssembly serverless functions, and then running them locally or deploying them to other runtimes. Spin will automatically download all of the supporting tools necessary for building Javascript apps into WebAssembly. You will need npm, though, if you don't already have it.
Spin has four important commands:
-
spin new
scaffolds out a new project. It currently supports several languages. Today we'll do Javascript. -
spin build
compiles a serverless function into WebAssembly. -
spin up
starts a local copy of our WebAssembly serverless function so we can test it out. -
spin deploy
can be used to deploy Spin to a production environment. There are also tools to deploy to Docker Desktop or to Kubernetes
We'll use all four.
To get started, let's build a new JS app. We're going to name it hello-dev
.
$ spin new http-js hello-dev --accept-defaults
Above, we've used spin new
to create a new http-js
application. That means we are writing an HTTP event handler in JavaScript.
Spin supports other handlers, like
redis
for listening on Redis pubsub queues.
The hello-dev
part is the name of our application. And the --accept-defaults
flag tells Spin not to ask us interactive questions, but to instead just build us an application using the default settings.
Once the command is done, we have a directory named hello-dev
. This is what we see inside of it:
$ tree hello-dev/
hello-dev/
├── README.md
├── package.json
├── spin.toml
├── src
│ └── index.js
└── webpack.config.js
1 directory, 5 files
Here's what those files are for:
-
README.md
is just the place where we describe our doc. -
package.json
is the usual NPM-style configuration file. -
spin.toml
is the Spin configuration file. We don't have to change anything there today, but it's an important piece of a Spin application. -
src/
contains our JS source, and we can see anindex.js
file there that we will edit in a bit. -
webpack.config.js
is the Webpack configuration file. When we build our app, Webpack is used to assemble it.
The first thing we need to do is change into the newly created hello-dev/
directory and run npm install
to set up the local environment for us.
$ cd hello-dev
$ npm install
added 124 packages, and audited 125 packages in 7s
17 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
That installs all of the core libraries we need for today's serverless function.
Next up, open src/index.js
in the editor of your choice. You'll see the following code:
const encoder = new TextEncoder()
export async function handleRequest(request) {
return {
status: 200,
headers: { "foo": "bar" },
body: encoder.encode("Hello from JS-SDK").buffer
}
}
This is just a very basic JS function. Spin will look for a function named handleRequest()
, so it is important that the function is named so. The request will be given an HTTP request object (here called request
), and Spin will expect that we return an HTTP response object, which we do in the code above using a literal object notion that has status
, headers
, and body
.
If you're new to HTTP,
status: 200
means success (404
meansnot found
and500
meansserver error
, and there are several others). Anything inheaders
will be accessible to the web browser, but not displayed to the user. andbody
will be displayed to the user in their browser.
Without making any edits to the code, we can build and test the application above.
First, we build with spin build
:
$ spin build
Building component hello-dev with `npm run build`
> hello-dev@1.0.0 build
> npx webpack --mode=production && mkdir -p target && spin js2wasm -o target/hello-dev.wasm dist/spin.js
asset spin.js 1.91 KiB [emitted] (name: main)
runtime modules 670 bytes 3 modules
./src/index.js 217 bytes [built] [code generated]
webpack 5.89.0 compiled successfully in 52 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
Once the above command is done, you should see a new directory called target/
, and inside that directory is a binary called hello-dev.wasm
.
$ tree target
target
└── hello-dev.wasm
0 directories, 1 file
Now we can use spin up
to start up our new hello-dev.wasm
serverless function.
$ spin up
Logging component stdio to ".spin/logs/"
Serving http://127.0.0.1:3000
Available Routes:
hello-dev: http://127.0.0.1:3000 (wildcard)
This starts a new local server listening on port 3000
. So you can point your browser at that URL and see the result:
That's our default serverless function! To stop the server, you can use CTRL-C
.
Next up, let's change the code. We'll make it a little shorter, and add a custom message.
There are two things about the original code that we can do away with.
- It's not actually necessary to use the
TextEncoder
to encode text. Spin does this for us. - We don't need a
header
in the response object.
So let's trim down the code and change the body
to be something a little more personalized to this post:
export async function handleRequest(request) {
return {
status: 200,
body: "Hello Dev.to Developers!"
}
}
We're down to a six line serverless function. Once more, we can use spin build
to build an updated WebAssembly binary, and then run spin up
to start a local server.
Once more pointing our browser to http://127.0.0.1:3000
, we can see our new text.
And there we have it! A Javascript serverless function in WebAssembly. If we wanted to deploy it to a server with a public URL, we could use spin deploy
. By default, that will send it to Fermyon Cloud (using the free tier), and you'll be prompted to log in with GitHub. But you can also point Spin to other deployment platforms.
Here's a quick example:
$ spin deploy
Uploading hello-dev version 0.1.0-rb6b8b1f3 to Fermyon Cloud...
Deploying...
Waiting for application to become ready........ ready
Available Routes:
hello-dev: https://hello-dev-ftv5h3fg.fermyon.app (wildcard)
Now I've got an app available on a public endpoint, and you can access it at https://hello-dev-ftv5h3fg.fermyon.app.
Next Steps
Not everything that you find in the NPM catalog of libraries will work. Spin does not provide a 100% Node compatible runtime. But Spin does provide some things that Node.js and other JS runtimes do not:
- Built-in Key Value Store means you don't have to solve the "stateless" problem. It's easy to store and retrieve text and JSON objects.
- Included SQLite Database provides a SQL storage layer along with data migrations.
- Serverless AI support means you can start coding applications that send prompts to LLMs like LLaMa2 and Code Llama. You can start writing AI apps very easily in Spin.
The best place to go from here is straight to the Spin Javascript Language Guide, where you can learn more about apps you can build.
Top comments (4)
Spin is quite interesting, but is it practical too?
You can get some ideas of what can be built here: developer.fermyon.com/hub
Cool, great post!
Can you explain what happens under the covers? How does Node.js code compile to a Wasm module with Spin?