Add Features
Going from "Hello World" to a more complex application starts with two steps:
- Identify the capabilities your application needs. (Think common requirements like serving HTTP, storing key-value pairs, or logging.)
- Add interfaces for those capabilities.
When you're writing a wasmCloud application, you don't have to worry about how a capability is fulfilled as long as you're writing to a standard interface—you can simply focus on your code.
In this tutorial, we'll add more features to our application by plugging in key-value and logging capabilities.
This tutorial assumes you're following directly from the previous tutorial. If you don't have a "Hello world" application running with wash dev
, complete Quickstart first.
Add functionality
- Go
- Rust
- TypeScript
Let's extend this application to do more than just say "Hello!"
Using the FormValue method on the incoming request, we can check the request for a name provided in a query string, and then return a greeting with that name. If there isn't one or the path isn't in the format we expect, we'll default to saying "Hello, World!"
//go:generate go run github.com/bytecodealliance/wasm-tools-go/cmd/wit-bindgen-go generate --world hello --out gen ./wit
import (
"fmt"
"net/http"
"go.wasmcloud.dev/component/net/wasihttp"
)
func init() {
wasihttp.HandleFunc(handleRequest)
}
func handleRequest(w http.ResponseWriter, r *http.Request) {
name := "World"
if len(r.FormValue("name")) > 0 {
name = r.FormValue("name")
}
fmt.Fprintf(w, "Hello, %s!\n", name)
fmt.Fprintf(w, "Hello from Go!\n")
}
// Since we don't run this program like a CLI, the `main` function is empty. Instead,
// we call the `handleRequest` function when an HTTP request is received.
func main() {}
Let's extend this application to do more than just say "Hello!"
Using methods on the incoming http::Request
, we can check the request for a name provided in a query string, and then return a greeting with that name. If there isn't one or the path isn't in the format we expect, we'll default to saying "Hello, World!"
use wasmcloud_component::http;
struct Component;
http::export!(Component);
impl http::Server for Component {
fn handle(
_request: http::IncomingRequest,
request: http::IncomingRequest,
) -> http::Result<http::Response<impl http::OutgoingBody>> {
let (parts, _body) = request.into_parts();
let query = parts
.uri
.query()
.map(ToString::to_string)
.unwrap_or_default();
let name = match query.split("=").collect::<Vec<&str>>()[..] {
["name", name] => name,
_ => "World",
};
Ok(http::Response::new(format!("Hello, {name}!\n")))
Ok(http::Response::new("Hello from Wasm!\n"))
}
}
Let's extend this application to do more than just say "Hello!"
Using the pathWithQuery
method on the incoming request, we can check the request for a name provided in a query string, and then return a greeting with that name. If there isn't one or the path isn't in the format we expect, we'll default to saying "hello world!"
To make this code more readable, we'll add a helper function to extract the name from the path.
import { IncomingRequest, ResponseOutparam, OutgoingBody, OutgoingResponse, Fields } from "wasi:http/types@0.2.0";
// Implementation of wasi-http incoming-handler
//
// NOTE: To understand the types involved, take a look at wit/deps/http/types.wit
function handle(req: IncomingRequest, resp: ResponseOutparam) {
// Start building an outgoing response
const outgoingResponse = new OutgoingResponse(new Fields());
// Access the outgoing response body
let outgoingBody = outgoingResponse.body();
{
// Create a stream for the response body
let outputStream = outgoingBody.write();
const name = getNameFromPath(req.pathWithQuery() || "")
// Write "hello [name]" to the response stream
outputStream.blockingWriteAndFlush(
new Uint8Array(new TextEncoder().encode("hello from Typescript"))
new Uint8Array(new TextEncoder().encode(`Hello ${name}!\n`))
);
// @ts-ignore: This is required in order to dispose the stream before we return
outputStream[Symbol.dispose]();
}
// Set the status code for the response
outgoingResponse.setStatusCode(200);
// Finish the response body
OutgoingBody.finish(outgoingBody, undefined);
// Set the created response
ResponseOutparam.set(resp, { tag: "ok", val: outgoingResponse });
}
function getNameFromPath(path: string): string {
const parts = path.split("=");
if (parts.length == 2) {
return parts[1];
}
return "world";
}
export const incomingHandler = {
handle,
};
After saving your changes, wash dev
automatically builds and runs the updated application.
We can curl
the application again:
curl localhost:8000
Hello, World!
curl 'localhost:8000?name=Bob'
Hello, Bob!
Add persistent storage
Now let's add persistent storage to keep a record of each person that this application greeted.
We'll use the key-value capability for this. We don't need to pick a library or a specific vendor implementation—all we have to do is add the interface to our component.
We can use the wasi:keyvalue
interface for interacting with a key value store, and the wasi:logging
interface to log the name of each person we greet. Before we can use those interfaces, we'll need to add them to our wit/world.wit
file:
- Go
- Rust
- TypeScript
package wasmcloud:hello;
world hello {
include wasmcloud:component-go/imports@0.1.0;
import wasi:logging/logging@0.1.0-draft;
import wasi:keyvalue/atomics@0.2.0-draft;
import wasi:keyvalue/store@0.2.0-draft;
export wasi:http/incoming-handler@0.2.0;
}
package wasmcloud:hello;
world hello {
import wasi:keyvalue/atomics@0.2.0-draft;
import wasi:keyvalue/store@0.2.0-draft;
import wasi:logging/logging@0.1.0-draft;
export wasi:http/incoming-handler@0.2.0;
}
package wasmcloud:hello;
world hello {
import wasi:keyvalue/atomics@0.2.0-draft;
import wasi:keyvalue/store@0.2.0-draft;
import wasi:logging/logging@0.1.0-draft;
export wasi:http/incoming-handler@0.2.0;
}
- Go
- Rust
- TypeScript
We've given our application the ability to perform atomic incrementation and storage operations via the wasi:keyvalue
interface.
Now let's use the atomic increment function to keep track of how many times we've greeted each person.
//go:generate go run github.com/bytecodealliance/wasm-tools-go/cmd/wit-bindgen-go generate --world hello --out gen ./wit
package main
import (
"fmt"
"net/http"
atomics "github.com/wasmcloud/wasmcloud/examples/golang/components/http-hello-world/gen/wasi/keyvalue/atomics"
store "github.com/wasmcloud/wasmcloud/examples/golang/components/http-hello-world/gen/wasi/keyvalue/store"
"go.wasmcloud.dev/component/log/wasilog"
"go.wasmcloud.dev/component/net/wasihttp"
)
func init() {
wasihttp.HandleFunc(handleRequest)
}
func handleRequest(w http.ResponseWriter, r *http.Request) {
logger := wasilog.ContextLogger("handleRequest")
name := "World"
if len(r.FormValue("name")) > 0 {
name = r.FormValue("name")
}
logger.Info("Greeting", "name", name)
kvStore := store.Open("default")
if err := kvStore.Err(); err != nil {
w.Write([]byte("Error: " + err.String()))
return
}
value := atomics.Increment(*kvStore.OK(), name, 1)
if err := value.Err(); err != nil {
w.Write([]byte("Error: " + err.String()))
return
}
fmt.Fprintf(w, "Hello x%d, %s!\n", *value.OK(), name)
fmt.Fprintf(w, "Hello, %s!\n", name)
}
// Since we don't run this program like a CLI, the `main` function is empty. Instead,
// we call the `handleRequest` function when an HTTP request is received.
func main() {}
Download the wasilog
module for idiomatic Go logging that compiles to a component:
go get go.wasmcloud.dev/component/log/wasilog
We've given our application the ability to perform atomic incrementation and storage operations via the wasi:keyvalue
interface and general logging operations via wasi:logging
.
Now let's use the atomic increment function to keep track of how many times we've greeted each person.
use wasmcloud_component::http;
use wasmcloud_component::http::ErrorCode;
use wasmcloud_component::wasi::keyvalue::*;
use wasmcloud_component::{http, info};
struct Component;
http::export!(Component);
impl http::Server for Component {
fn handle(
request: http::IncomingRequest,
) -> http::Result<http::Response<impl http::OutgoingBody>> {
let (parts, _body) = request.into_parts();
let query = parts
.uri
.query()
.map(ToString::to_string)
.unwrap_or_default();
let name = match query.split("=").collect::<Vec<&str>>()[..] {
["name", name] => name,
_ => "World",
};
info!("Greeting {name}");
let bucket = store::open("default").map_err(|e| {
ErrorCode::InternalError(Some(format!("failed to open KV bucket: {e:?}")))
})?;
let count = atomics::increment(&bucket, &name, 1).map_err(|e| {
ErrorCode::InternalError(Some(format!("failed to increment counter: {e:?}")))
})?;
Ok(http::Response::new(format!("Hello x{count}, {name}!\n")))
}
}
We've given our application the ability to perform atomic incrementation and storage operations via the wasi:keyvalue
interface and general logging operations via wasi:logging
.
Now let's use the atomic increment function to keep track of how many times we've greeted each person.
At the time of writing, JCO does not generate types for wasi:logging
or wasi:keyvalue
. This is
a known issue and will be resolved in a future release. For now, you can tell the TypeScript
compiler to ignore the missing types by adding //@ts-expect-error
before each import statement.
Simply including the import statement will allow the host to provider the functionality at runtime.
import {
IncomingRequest,
ResponseOutparam,
OutgoingBody,
OutgoingResponse,
Fields,
} from 'wasi:http/types@0.2.0';
//@ts-expect-error -- these types aren't currently generated by JCO
import { log } from 'wasi:logging/logging';
//@ts-expect-error -- these types aren't currently generated by JCO
import { increment } from 'wasi:keyvalue/atomics@0.2.0-draft';
// @ts-expect-error -- these types aren't currently generated by JCO
import { open } from 'wasi:keyvalue/store@0.2.0-draft';
// Implementation of wasi-http incoming-handler
//
// NOTE: To understand the types involved, take a look at wit/deps/http/types.wit
function handle(req: IncomingRequest, resp: ResponseOutparam) {
// Start building an outgoing response
const outgoingResponse = new OutgoingResponse(new Fields());
// Access the outgoing response body
let outgoingBody = outgoingResponse.body();
// Write to the response stream
const name = getNameFromPath(req.pathWithQuery() || '');
log('info', '', `Greeting ${name}`);
// Increment the bucket's count
const bucket = open('default');
const count = increment(bucket, name, 1);
{
// Create a stream for the response body
let outputStream = outgoingBody.write();
// Write hello world to the response stream
outputStream.blockingWriteAndFlush(
new Uint8Array(new TextEncoder().encode(`Hello x${count}, ${name}!\n`)),
);
// @ts-ignore: This is required in order to dispose the stream before we return
outputStream[Symbol.dispose]();
}
// Set the status code for the response
outgoingResponse.setStatusCode(200);
// Finish the response body
OutgoingBody.finish(outgoingBody, undefined);
// Set the created response
ResponseOutparam.set(resp, { tag: 'ok', val: outgoingResponse });
}
We've made changes, so once we save, wash dev
will once again automatically update the running application.
curl 'localhost:8000?name=Bob'
Hello x1, Bob!
curl 'localhost:8000?name=Bob'
Hello x2, Bob!
curl 'localhost:8000?name=Alice'
Hello x1, Alice!
Next steps
In this tutorial, you added a few more features and persistent storage to a simple microservice. You also got to see the process of developing with capabilities, where you can...
- Write purely functional code that doesn't require you to pick a library or vendor upfront
- Change your application separately from its non-functional requirements
So far, the wash dev
process has satisfied our application's capability requirements automatically, so we can move quickly and focus on code. In the next tutorial, we'll look under the hood and learn how to extend and deploy applications manually, plugging in different providers to deliver our capabilities.