Skip to main content
Version: v2.0.0-rc

Develop a Wasm Component

Once you know what your application needs to be able to do, you can add language-agnostic interfaces for common capabilities like HTTP, key-value storage, or logging.

In this step, we'll discuss how to:

  • Update our application to accept a name and return a personalized greeting.
  • Add more features to our application by plugging in key-value and logging capabilities.
Prerequisites

This tutorial assumes you're following directly from the previous tutorial. If you don't have a "Hello world" application running, complete Quickstart first.

Add functionality

Let's extend this application to do more than just say "Hello!"

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!"

rust
use wstd::http::{Body, Request, Response, StatusCode};

#[wstd::http_server]
async fn main(req: Request<Body>) -> Result<Response<Body>, wstd::http::Error> {
    match req.uri().path_and_query().unwrap().as_str() {
        "/" => home(req).await,
        _ => not_found(req).await,
    }
}

async fn home(req: Request<Body>) -> Result<Response<Body>, wstd::http::Error> {
  // Parse query string for name parameter
    let query = req.uri().query().unwrap_or(""); 
    let name = match query.split("=").collect::<Vec<&str>>()[..] { 
            ["name", name] => name, 
            _ => "World", 
        }; 
  // Return a simple response with a string body
    Ok(Response::new("Hello from Wasm!\n".into())) 
    Ok(Response::new(format!("Hello, {name}!\n").into())) 
}

async fn not_found(_req: Request<Body>) -> Result<Response<Body>, wstd::http::Error> {
    Ok(Response::builder()
        .status(StatusCode::NOT_FOUND)
        .body("Not found\n".into())
        .unwrap())
}

After saving your changes, your toolchain automatically builds and runs the updated application.

We can curl the application again:

shell
curl localhost:8000
text
Hello, World!
shell
curl 'localhost:8000?name=Bob'
text
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.

Before we can use the interface, we'll need to add a wit/world.wit file:

sh
touch wit/world.wit
wit
package wasmcloud:templates@0.1.0;

world rust-http-hello-world {
    import wasi:keyvalue/store@0.2.0-draft;
    import wasi:keyvalue/atomics@0.2.0-draft;
}

We've given our application the ability to perform atomic incrementation and storage operations via the wasi:keyvalue interface.

Now we need to add wit-bindgen to our Cargo.toml so we can generate Rust bindings from the WIT world:

toml
[dependencies]
wstd = "0.6.3"
wit-bindgen = "0.46.0"

Now let's use the atomic increment function to keep track of how many times we've greeted each person.

rust
use wstd::http::{Body, Request, Response, StatusCode};

// Generate keyvalue bindings from our WIT world
// wstd handles the http export via its macro
wit_bindgen::generate!({ 
    world: "rust-http-hello-world", 
    path: "wit", 
    generate_all, 
}); 

use wasi::keyvalue::{atomics, store}; 

#[wstd::http_server]
async fn main(req: Request<Body>) -> Result<Response<Body>, wstd::http::Error> {
    match req.uri().path_and_query().unwrap().as_str() {
        "/" => home(req).await,
        _ => not_found(req).await,
    }
}

async fn home(req: Request<Body>) -> Result<Response<Body>, wstd::http::Error> {
    // Parse query string for name parameter
    let query = req.uri().query().unwrap_or("");
    let name = match query.split("=").collect::<Vec<&str>>()[..] {
            ["name", name] => name,
            _ => "World",
        };
    // Open keyvalue bucket and increment counter for this name
    // Note: wasmtime's in-memory keyvalue provider requires empty string as identifier
    let bucket = store::open("") 
        .map_err(|e| wstd::http::Error::msg(format!("keyvalue open error: {:?}", e)))?; 

    let count = atomics::increment(&bucket, &name, 1) 
        .map_err(|e| wstd::http::Error::msg(format!("keyvalue increment error: {:?}", e)))?; 

    // Return greeting with count
    Ok(Response::new(format!("Hello x{count}, {name}!\n").into())) 
    // Return a simple response with a string body
    Ok(Response::new(format!("Hello, {name}!\n").into())) 
}

async fn not_found(_req: Request<Body>) -> Result<Response<Body>, wstd::http::Error> {
    Ok(Response::builder()
        .status(StatusCode::NOT_FOUND)
        .body("Not found\n".into())
        .unwrap())
}

We've made changes, so once we save, the toolchain will once again automatically update the running application.

Let's curl the component again:

shell
curl 'localhost:8000?name=Bob'
text
Hello x1, Bob!
shell
curl 'localhost:8000?name=Bob'
text
Hello x2, Bob!
shell
curl 'localhost:8000?name=Alice'
text
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, wash has satisfied our application's capability requirements automatically, so we can move quickly and focus on code. In the next tutorial, we'll deploy WebAssembly workloads to wasmCloud on a local Kubernetes cluster.