Skip to main content
Version: 0.82

CRUD with WASI Key-Value

Build and deploy a CRUDdy Wasm component

This tutorial will show you how to create and deploy a distributed WebAssembly application with wasmCloud, using Python, Rust, Go, or TypeScript. Building on what you've learned in the Quickstart, we'll explore the fundamentals of wasmCloud development by making a simple guestbook that performs CRUD operations using several capabilities—abstracted requirements like HTTP handling or key-value storage, fulfilled by WebAssembly components.

By the end, you'll understand how you can use wasmCloud to orchestrate components and build a distributed application. Along the way, you'll learn how to create a component from scratch and how to use WebAssembly Standard Interface (WASI) APIs.

Let's get going!

Wait, what is a WebAssembly component?

A component is portable Wasm code that is interoperable with any other component, regardless of the language in which each component was written. The interoperability of WebAssembly components drives wasmCloud's "building blocks" approach—you can write original code in any language that compiles to WebAssembly and then plug in the capabilities you need (e.g. HTTP) without worrying about how that functionality is implemented. Learn more about WebAssembly components in the Concepts section.

Before you get started

In this tutorial, we'll be using two core tools at the heart of wasmCloud: the wasmCloud Shell (wash) and wasmCloud Application Deployment Manager (wadm).

  • wash provides a command-line interface for wasmCloud, helping you run hosts, build and deploy components, and manage your installation.
  • wadm orchestrates wasmCloud workloads according to declarative manifests.

If you haven't installed wasmCloud already, head on over to the installation instructions and then return here.

Versions and compatibility

This tutorial uses wasmCloud v0.82 and first-party providers designed for WASI HTTP 0.2.0 and WASI Key-Value Store 0.1.0. You shouldn't have to worry about WASI API versions, since the API definitions come packaged with the wasmCloud templates used here.

You will also need both the Rust toolchain and the wasm32-wasi target installed on the same machine. wash depends on the Rust toolchain to compile Rust code to Wasm.

shell
rustup target add wasm32-wasi

Creating a new component

In wasmCloud, an application component is a WebAssembly component dedicated to an application's creative logic. Typically, we will simply refer to this as a "component." Historically, wasmCloud referred to this piece of an application as an "actor," but that term is deprecated in favor of "component" and CLI commands will change as of v1.0.

With wash installed, you can run the following command in your shell to create a new component from a simple template:

shell
wash new actor cruddy-rust --git wasmcloud/wasmcloud --subfolder examples/rust/actors/http-hello-world --branch 0.82-examples

This command instructs wash to create a new component called "cruddy-rust". For our template, we're using the http-hello-world template from the 0.82 branch of the wasmCloud project repo on GitHub. The new actor command always creates new components from templates—if you don't specify one from the outset, wash will provide you with a set of options.

The repo we're using gives us the skeleton for a new component in Rust. Let's take a look at the cruddy-rust directory:

shell
├── Cargo.lock
├── Cargo.toml
├── README.md
├── src
│   └── lib.rs
├── wadm.yaml
├── wasmcloud.toml
└── wit

Here we have the standard Rust project files as well as three pieces that make up a wasmCloud application component:

  • wadm.yaml is a declarative deployment manifest used by wadm.
  • wasmcloud.toml is a metadata and permissions configuration file for the application component.
  • The wit directory holds WebAssembly Interface Type (WIT) definitions for standard APIs used across the Wasm ecosystem—we'll discuss these at greater length in a moment.

Let's open wasmcloud.toml and have a look around:

toml
name = "http-hello-world"
language = "rust"
type = "actor"
version = "0.1.0"

[actor]
wit_world = "hello"
wasm_target = "wasm32-wasi-preview2"

Here we have metadata for details like naming and versioning. For now, we're most interested in the [actor] fields:

  • wit_world points the application to a set of interfaces (known as a "world") defined in the wit directory.
  • wasm_target specifies the Wasm compilation target.

Now let's take a look at the wit directory.

Understanding WIT dependencies

We will interact with the httpserver and keyvalue capabilities via WebAssembly System Interface (WASI) APIs—ecosystem-standard APIs used for communication with and between components.

Where does WASI fit in the development cycle?

WASI is especially useful for building and maintaining standard and popular libraries in an efficient and language-agnostic way—so developers can focus on writing simple, portable, idiomatic code.

Take a look at the contents of wit:

shell
├── deps
├── deps.toml
└── world.wit
  • deps is a directory that holds WIT definitions.
  • deps.toml is a TOML configuration file used to manage WIT dependencies.
  • world.wit defines the interfaces that comprise your WIT "world."

Our template has pre-populated the wit directory with relevant definitions. However, if you wish to select your WIT dependencies and populate the directory yourself, you can use the wit-deps tool.

Open deps.toml. Here we have specified interfaces that we wish to utilize in our app. WASI interfaces like wasi-http or wasi-keyvalue are available to browse in the WebAssembly project's GitHub repo.

WIT definitions contain documentation as comments within the WIT files themselves. It is very useful to read through the WIT comprehensively before using a new interface.

Take a look at /deps/http/types.wit. Here you'll find a detailed explanation of the interface's types. Note the outgoing-request type—it will come up again in a moment.

wit
/// Represents an outgoing HTTP Request.
resource outgoing-request

With our WIT dependencies in place, we can specify interface imports and exports in world.wit. A WIT world is a set of standard interfaces that you can use to interact with components via defined functions and types. The WIT interfaces don't provide functionality in themselves—instead, they provide a common language for the contracts between components.

Remember that we targeted the hello world in our wasmcloud.toml file; this is where that world is defined. Let's update the hello world in world.wit:

wit
package wasmcloud:hello;

world hello {
  import wasi:keyvalue/eventual@0.1.0; 

  export wasi:http/incoming-handler@0.2.0;
}

The wasmcloud.toml file points to the hello world in world.wit, and the hello world connects to our WIT definitions.

Now that our dependencies are defined, we're ready to get coding.

Working with WASI APIs

Open src/lib.rs.

rust
wit_bindgen::generate!({
    world: "hello",
    exports: {
        "wasi:http/incoming-handler": HttpServer,
    },
});

use exports::wasi::http::incoming_handler::Guest;
use wasi::http::types::*;

struct HttpServer;

impl Guest for HttpServer {
    fn handle(_request: IncomingRequest, response_out: ResponseOutparam) {
        let response = OutgoingResponse::new(Fields::new());
        response.set_status_code(200).unwrap();
        let response_body = response.body().unwrap();
        response_body
            .write()
            .unwrap()
            .blocking_write_and_flush(b"Hello from Rust!\n")
            .unwrap();
        OutgoingBody::finish(response_body, None).expect("failed to finish response body");
        ResponseOutparam::set(response_out, Ok(response));
    }
}

Most of this file is simple boilerplate. We've made WASI APIs available, but how do we use those APIs in Rust?

The answer is in the first lines. When we build the application with the wash build command, the builder will use these instructions to run the built-in wit-bindgen tool and generate bindings between the functions we saw in the WIT dependencies and Rust.

Let's try it out. In the root of the project directory:

shell
wash build

This will create a build directory with a compiled .wasm artifact for the app. It will also create a target directory including dependencies such as the generated bindings.

In Rust, using WASI APIs is fairly intuitive, especially with IDE suggestions and autofills. If we've read through the keyvalue WIT, we know that we will need an instance of the OutgoingValue type later. We can create it this way:

rust
// In the language of WASI, here we're creating a new outgoing-value resource
let value: OutgoingValue =
  wasi::keyvalue::types::OutgoingValue::new_outgoing_value();

At each stage from wasi:: to keyvalue:: to types:: and beyond, our IDE can provide us with the available options as documented in the WIT. Taken in conjunction, our WIT files and code completion gives us the tools we need to use the API and put the pieces together. For now, put this at the top of our handler function.

So what else do we have in lib.rs?

Most of the code is in a handle function responding to requests:

rust
impl Guest for HttpServer {
    fn handle(_request: IncomingRequest, response_out: ResponseOutparam) {
        let response = OutgoingResponse::new(Fields::new());
        response.set_status_code(200).unwrap();
        let response_body = response.body().unwrap();
        response_body
            .write()
            .unwrap()
            .blocking_write_and_flush(b"Hello from Rust!\n")
            .unwrap();
        OutgoingBody::finish(response_body, None).expect("failed to finish response body");
        ResponseOutparam::set(response_out, Ok(response));
    }
}

Now, at the top of the handle function, we'll use the WASI HTTP API's path_with_query method on our incoming request—much like in the Quickstart. This will extract and return a name provided in incoming HTTP queries. (While we're at it, we'll delete the response body, as we'll be replacing that soon.)

rust
impl Guest for HttpServer {
    fn handle(_request: IncomingRequest, response_out: ResponseOutparam) { 
    fn handle(request: IncomingRequest, response_out: ResponseOutparam) { 
        let response = OutgoingResponse::new(Fields::new());
        response.set_status_code(200).unwrap();
        let response_body = response.body().unwrap();
        let name = match request 
            .path_with_query()
            .unwrap()
            .split("=")
            .collect::<Vec<&str>>()[..]
        {
            // query string is "/?name=<name>" e.g. localhost:8080?name=Bob
            ["/?name", name] => name.to_string(),
            // query string is anything else or empty e.g. localhost:8080
            _ => "Anonymous".to_string(),
        };
        response_body 
            .write() 
            .unwrap() 
            .blocking_write_and_flush(b"Hello from Rust!\n") 
            .unwrap(); 
        OutgoingBody::finish(response_body, None).expect("failed to finish response body"); 
        ResponseOutparam::set(response_out, Ok(response)); 

This is the first time we're actually implementing a new WASI method in this component, so we should pause to consider exactly what we're doing:

  • When we look at our WIT bindings, we can see that IncomingRequest has a method path_with_query(). The original WIT tell us that path_with_query() returns a string of the request path, including any query. However, the string is contained within a return object and needs to be "unwrapped."
  • To get at the actual value, we use the unwrap() method.
  • To isolate the name, we split the value at the = and collect it as a <Vec<&str>> that we can use elsewhere.

In order to guide our CRUD operations for the guestbook, we'd also like to know the method of the incoming request. We can detect that with the API as well. Try finding the method of the request with what you've learned so far.

If you guessed that you could use a method on request, well done! Below the previous line add:

rust
// Detect method
let method = request.method(); 

This will return a method of the Method type, which we will be able to use directly.

We've implemented our basic HTTP operations. Now let's turn to key-value storage.

Adding CRUD

We can follow the same procedure we used for wasi-http to explore the wasi-keyvalue API and start adding CRUD operations.

In /wit/deps/keyvalue/types.wit, we see that collections of key-value pairs are referenced as buckets in the API:

wit
/// A bucket is a collection of key-value pairs. Each key-value pair is stored
/// as a entry in the bucket, and the bucket itself acts as a collection of all
/// these entries.
///
/// It is worth noting that the exact terminology for bucket in key-value stores
/// can very depending on the specific implementation. For example,
/// 1. Amazon DynamoDB calls a collection of key-value pairs a table
/// 2. Redis has hashes, sets, and sorted sets as different types of collections
/// 3. Cassandra calls a collection of key-value pairs a column family
/// 4. MongoDB calls a collection of key-value pairs a collection
/// 5. Riak calls a collection of key-value pairs a bucket
/// 6. Memcached calls a collection of key-value pairs a slab
/// 7. Azure Cosmos DB calls a collection of key-value pairs a container
///
/// In this interface, we use the term `bucket` to refer to a collection of key-value

We'd like to open a bucket to store the names of our guests, so we'll add the following to our handler function between the method and value assignments:

rust
// Open keyvaluestore bucket
// In the language of WASI, here we're creating a new bucket resource
let bucket =
    wasi::keyvalue::types::Bucket::open_bucket("").expect("failed to open empty bucket"); 

Next we'll create a variable called value of the OutgoingValue type that will hold the new values assigned to keys in PUT/set operations. This is a wasi-keyvalue pattern that we would use in any language: the new_outgoing_value() method is creating a new resource, which we can use to perform synchronous write operations.

rust
// Create a variable for outgoing values - Put/sets will use this
// In the language of WASI, here we're creating a new outgoing-value resource
let value: OutgoingValue =
    wasi::keyvalue::types::OutgoingValue::new_outgoing_value(); 

Now we'll add a switch case that creates/updates, reads, or destroys guestbook records based on the HTTP method of the incoming request. For this rudimentary app, each case will send an appropriate HTTP response:

rust
match method { 
    wasi::http::types::Method::Get => { 

        // If the key doesn't exist, then the value will be returned as none, so we're checking for noneness to handle errors
        let none = wasi::keyvalue::eventual::get(&bucket, &name).unwrap().is_none(); 

        if none { 
            // Send error message if the value is none
            response_body 
                .write() 
                .unwrap() 
                .blocking_write_and_flush(format!("{name} not found").as_bytes()) 
                .unwrap(); 
            OutgoingBody::finish(response_body, None).expect("failed to finish response body"); 
            ResponseOutparam::set(response_out, Ok(response)); 
        } else { 

            let result = wasi::keyvalue::eventual::get(&bucket, &name).unwrap().unwrap(); 

            // Take this result (of the IncomingValue type) and consume that value to get a uint8 byte array, which we will then send as part of http response

            let in_val = wasi::keyvalue::types::IncomingValue::incoming_value_consume_sync(result).unwrap(); 

            // Send HTTP response
            response_body 
                .write() 
                .unwrap() 
                .blocking_write_and_flush(&in_val).unwrap(); 
            OutgoingBody::finish(response_body, None).expect("failed to finish response body"); 
            ResponseOutparam::set(response_out, Ok(response)); 
        }; 

    }, 
    wasi::http::types::Method::Put => { 
        // Assign value as byte array
        let writval = format!("attending").as_bytes().to_vec(); 

        // Use outgoingvaluewritebodysync to actually write the value
        let _ = value.outgoing_value_write_body_sync(&writval); 

        wasi::keyvalue::eventual::set(&bucket, &name, &value) 
                .expect("failed to set"); 

        // Send HTTP response
        response_body 
            .write() 
            .unwrap() 
            .blocking_write_and_flush(format!("Added {name}").as_bytes()) 
            .unwrap(); 
        OutgoingBody::finish(response_body, None).expect("failed to finish response body"); 
        ResponseOutparam::set(response_out, Ok(response)); 
    }, 
    wasi::http::types::Method::Delete => { 

        // If the key doesn't exist, then the value will be returned as none, so we're checking for noneness to handle errors
        let none = wasi::keyvalue::eventual::get(&bucket, &name).unwrap().is_none();

        if none {
            // Send error message if the value is none
            response_body
                .write()
                .unwrap()
                .blocking_write_and_flush(format!("{name} not found").as_bytes())
                .unwrap();
            OutgoingBody::finish(response_body, None).expect("failed to finish response body");
            ResponseOutparam::set(response_out, Ok(response));
        } else {
            // Delete key
            let _ = wasi::keyvalue::eventual::delete(&bucket, &name);

            // Send HTTP response
            response_body
                .write()
                .unwrap()
                .blocking_write_and_flush(format!("It's like {name} was never there").as_bytes())
                .unwrap();
            OutgoingBody::finish(response_body, None).expect("failed to finish response body");
            ResponseOutparam::set(response_out, Ok(response));
        };
    } 
    _ => { 
        // Handle other cases
        // Send HTTP response
        response_body 
            .write() 
            .unwrap() 
            .blocking_write_and_flush(format!("Try sending a GET, PUT, or DELETE").as_bytes()) 
            .unwrap(); 
        OutgoingBody::finish(response_body, None).expect("failed to finish response body"); 
        ResponseOutparam::set(response_out, Ok(response)); 
    } 
}; 

Most of our application logic is happening in this block, so let's pause to consider a few important elements.

The condition for each case is the request method returned to the method variable. Within the conditionals, each CRUD operation runs against bucket and name. In the case of wasi::keyvalue::eventual::delete, that's extremely straightforward, since no data is being passed. The set and get operations are just a touch more complicated.

For the set, we use the outgoing_value_write_body_sync method to write the body of the value (defined as a Vec of bytes in writval) to value. With our value written to value, we can pass it into wasi::keyvalue::eventual::set.

The get logic is similar but runs in reverse. wasi::keyvalue::eventual::get gives us a result that must be unwrapped and then consumed with wasi::keyvalue::types::IncomingValue::incoming_value_consume_sync, which gives us a byte array. Byte arrays are a common unit of data in WASI interfaces—note how we use them in HTTP responses as well. Since we have a byte array from our incoming value now, we can pass it directly to the HTTP response.

At this point, lib.rs should look like this:

rust
wit_bindgen::generate!({
    world: "hello",
    exports: {
        "wasi:http/incoming-handler": HttpServer,
    },
});

use exports::wasi::http::incoming_handler::Guest;
use wasi::{http::types::*, keyvalue::types::OutgoingValue};

struct HttpServer;

impl Guest for HttpServer {
    fn handle(request: IncomingRequest, response_out: ResponseOutparam) {
        let response = OutgoingResponse::new(Fields::new());
        response.set_status_code(200).unwrap();
        let response_body = response.body().unwrap();

        // Get any name appended to the path via query
        let name = match request
            .path_with_query()
            .unwrap()
            .split("=")
            .collect::<Vec<&str>>()[..]
        {
            // query string is "/?name=<name>" e.g. localhost:8080?name=Bob
            ["/?name", name] => name.to_string(),
            // query string is anything else or empty e.g. localhost:8080
            _ => "Anonymous".to_string(),
        };

        // Detect method
        let method = request.method();

    	// Open keyvaluestore bucket
	    // In the language of WASI, here we're creating a new bucket resource
        let bucket =
            wasi::keyvalue::types::Bucket::open_bucket("").expect("failed to open empty bucket");

        // Create a variable for outgoing values - Put/sets will use this
	    // In the language of WASI, here we're creating a new outgoing-value resource
        let value: OutgoingValue =
            wasi::keyvalue::types::OutgoingValue::new_outgoing_value();

        // Switch case on method

        match method {
            wasi::http::types::Method::Get => {

                // If the key doesn't exist, then the value will be returned as none, so we're checking for noneness to handle errors
                let none = wasi::keyvalue::eventual::get(&bucket, &name).unwrap().is_none();

                if none {
                    // Send error message if the value is none
                    response_body
                        .write()
                        .unwrap()
                        .blocking_write_and_flush(format!("{name} not found").as_bytes())
                        .unwrap();
                    OutgoingBody::finish(response_body, None).expect("failed to finish response body");
                    ResponseOutparam::set(response_out, Ok(response));
                } else {

                    let result = wasi::keyvalue::eventual::get(&bucket, &name).unwrap().unwrap();

                    // Take this result (of the IncomingValue type) and consume that value to get a uint8 byte array, which we will then send as part of http response

                    let in_val = wasi::keyvalue::types::IncomingValue::incoming_value_consume_sync(result).unwrap();

                    // Send HTTP response
                    response_body
                        .write()
                        .unwrap()
                        .blocking_write_and_flush(&in_val).unwrap();
                    OutgoingBody::finish(response_body, None).expect("failed to finish response body");
                    ResponseOutparam::set(response_out, Ok(response));
                };

            },
            wasi::http::types::Method::Put => {
                // Assign value as byte array
                let writval = format!("attending").as_bytes().to_vec();

                // Use outgoingvaluewritebodysync to actually write the value
                let _ = value.outgoing_value_write_body_sync(&writval);

                wasi::keyvalue::eventual::set(&bucket, &name, &value)
                        .expect("failed to set");

                // Send HTTP response
                response_body
                    .write()
                    .unwrap()
                    .blocking_write_and_flush(format!("Added {name}").as_bytes())
                    .unwrap();
                OutgoingBody::finish(response_body, None).expect("failed to finish response body");
                ResponseOutparam::set(response_out, Ok(response));
            },
            wasi::http::types::Method::Delete => {

                // If the key doesn't exist, then the value will be returned as none, so we're checking for noneness to handle errors
                let none = wasi::keyvalue::eventual::get(&bucket, &name).unwrap().is_none();

                if none {
                    // Send error message if the value is none
                    response_body
                        .write()
                        .unwrap()
                        .blocking_write_and_flush(format!("{name} not found").as_bytes())
                        .unwrap();
                    OutgoingBody::finish(response_body, None).expect("failed to finish response body");
                    ResponseOutparam::set(response_out, Ok(response));
                } else {
                    // Delete key
                    let _ = wasi::keyvalue::eventual::delete(&bucket, &name);

                    // Send HTTP response
                    response_body
                        .write()
                        .unwrap()
                        .blocking_write_and_flush(format!("It's like {name} was never there").as_bytes())
                        .unwrap();
                    OutgoingBody::finish(response_body, None).expect("failed to finish response body");
                    ResponseOutparam::set(response_out, Ok(response));
                };
            }
            _ => {
                // Handle other cases
                // Send HTTP response
                response_body
                    .write()
                    .unwrap()
                    .blocking_write_and_flush(format!("Try sending a GET, PUT, or DELETE").as_bytes())
                    .unwrap();
                OutgoingBody::finish(response_body, None).expect("failed to finish response body");
                ResponseOutparam::set(response_out, Ok(response));
            }
        };

    }
}

Since we've made changes to lib.rs, we'll build again to update the .wasm artifact:

shell
wash build

Start Redis

Up front, we said that we don't have to worry about the underlying software that provides a capability until we deploy. Now it's time to start thinking about which capability providers we want to use. You can explore the list of available capability providers in the wasmCloud GitHub repo. For key-value storage, we'll use Redis.

Start a Redis server with either redis-server or Docker:

shell
redis-server &

docker run -d --name redis -p 6379:6379 redis

Prepare for deployment

The last step to deploy our component is preparing the declarative manifest in wadm.yaml, which defines the desired state for our application when it is running on wasmCloud. This will look familiar if you've used container orchestrators like Kubernetes. The manifest included with the hello-world-rust template looks like this:

yaml
apiVersion: core.oam.dev/v1beta1
kind: Application
metadata:
  name: rust-http-hello-world
  annotations:
    version: v0.0.1
    description: 'HTTP hello world demo in Rust, using the WebAssembly Component Model and WebAssembly Interfaces Types (WIT)'
    experimental: 'true'
spec:
  components:
    - name: http-hello-world
      type: actor
      properties:
        image: file://./build/http_hello_world_s.wasm
      traits:
        # Govern the spread/scheduling of the actor
        - type: spreadscaler
          properties:
            replicas: 1
        # Link the HTTP server, and inform it to listen on port 8080
        # on the local machine
        - type: linkdef
          properties:
            target: httpserver
            values:
              ADDRESS: 127.0.0.1:8080

    # Add a capability provider that mediates HTTP access
    - name: httpserver
      type: capability
      properties:
        image: wasmcloud.azurecr.io/httpserver:0.19.1
        contract: wasmcloud:httpserver

The metadata fields provide naming and versioning for our application. We'll update the name to describe what we've built:

yaml
apiVersion: core.oam.dev/v1beta1
kind: Application
metadata:
  name: rust-http-hello-world
  name: cruddy
  annotations:
    version: v0.0.1
    description: "HTTP hello world demo in Rust, using the WebAssembly Component Model and WebAssembly Interfaces Types (WIT)"
	description: "CRUD demo"
    experimental: "true"
Note

Every time we publish a new version, we'll need to increment (or otherwise change) the version annotation here.

Next in the spec, we have our components: the http-hello-world templated component that we've been working on, and the httpserver capability fulfilled by a capability provider. We'll explore capabilities more in a moment, but first, we'll give the component a link to the keyvalue capability, specifying the address for the Redis service:

yaml
spec:
  components:
    - name: http-hello-world
      type: actor
      properties:
        image: file://./build/http_hello_world_s.wasm
      traits:
        - type: spreadscaler
          properties:
            replicas: 1
        - type: linkdef
          properties:
            target: httpserver
            values:
              address: 0.0.0.0:8080
        - type: linkdef
          properties: 
            target: keyvalue
            values: 
              address: redis://127.0.0.1:6379

Finally, we'll add the keyvalue capability provider as the final component that makes up our application. As part of this entry, we'll specify that the component—provided through the defined image—is fulfilling the contract for the keyvalue capability. wasmCloud operates on a zero-trust security model: without being explicitly linked to a capability, the application component would have no access to it.

yaml
- name: httpserver
  type: capability
  properties:
    image: wasmcloud.azurecr.io/httpserver:0.19.1
    contract: wasmcloud:httpserver
- name: keyvalue
  type: capability
  properties: 
    image: wasmcloud.azurecr.io/kvredis:0.22.0
    contract: wasmcloud:keyvalue

We effectively promised our application component that someone would get the job done, and now these first-party providers (developed as part of the wasmCloud project) are stepping up to fill the role. A different provider—first-party, third-party, or of original design—could just as easily do the same. The same component we wrote could perform CRUDdy operations against any number of different key-value stores—etcd, MongoDB, Cassandra, or your own—as long as a provider exists for it. If a provider doesn't exist yet, you have all the tools you need to create one.

Once you put all of the pieces of wadm.yaml together, the file should look like this:

yaml
apiVersion: core.oam.dev/v1beta1
kind: Application
metadata:
  name: cruddy
  annotations:
    version: v0.0.1
    description: 'CRUD demo'
    experimental: 'true'
spec:
  components:
    - name: http-hello-world
      type: actor
      properties:
        image: file://./build/http_hello_world_s.wasm
      traits:
        - type: spreadscaler
          properties:
            replicas: 1
        - type: linkdef
          properties:
            target: httpserver
            values:
              address: 0.0.0.0:8080
        - type: linkdef
          properties:
            target: keyvalue
            values:
              address: redis://127.0.0.1:6379
    - name: httpserver
      type: capability
      properties:
        image: wasmcloud.azurecr.io/httpserver:0.19.1
        contract: wasmcloud:httpserver
    - name: keyvalue
      type: capability
      properties:
        image: wasmcloud.azurecr.io/kvredis:0.22.0
        contract: wasmcloud:keyvalue

Launch and iterate

Start a wasmCloud host with wash up. Now we're ready to launch our app:

shell
wash app deploy wadm.yaml

To view your wasmCloud apps and check status:

shell
wash app list

Once the app status is Deployed, we can test a PUT against our app with curl:

shell
curl -X PUT "localhost:8080?name=Alice"

We should get the result:

shell
Added Alice

We can test a GET and DELETE as well:

shell
curl "localhost:8080?name=Alice"
attended
curl -X DELETE "localhost:8080?name=Alice"
It's like Alice was never there

If we want to update our application, we can wash build, update the version in wadm.yaml, and wash app deploy wadm.yaml again.

Note that wasmCloud includes an experimental feature for dev loop iteration. Rather than wash app deploy, from your project directory you can run:

shell
wash dev --experimental --host-id=<your-host-id>

This will start a dev deployment that continuously watches for changes to the .wasm file for your component. If you would like to try this experimental feature, first clean up your existing deployment according to the instructions below.

Clean up

Once you're finished working with these tutorial materials, undeploy the application to stop it from running on the wasmCloud host and delete active links, freeing up associated ports:

shell
wash app undeploy cruddy

To completely remove all versions of the application from wasmCloud:

shell
wash app delete cruddy --delete-all

Next steps

In this tutorial, you've learned how to create a guestbook application component that uses WASI APIs including HTTP and Key-Value. In doing so, you've also learned how to read the WIT definitions for those APIs and utilize idiomatic bindings for our chosen language. With these fundamentals in place, a good next step might be to try building more complex components, explore other WASI APIs, or create your own interfaces and providers. If you have questions or feedback, join the wasmCloud Slack and let us know. Happy coding!