Understand your first capsule
The packaged application walkthrough
creates a greeting program: send Ada, receive Hello, Ada!. This page explains
the files you created and how an input becomes a typed answer. You can write the
same capsule in Rust, C, TypeScript, Go, Java or C#.
1. Read the contract
Open the WIT contract under your project's app directory. The greeting exports:
greet: func(name: string) -> result<string, string>;
name: string is the input. result<string, string> means the function returns
either a greeting or an explanation of invalid input. An export is a
function callers can use. An import is a function your capsule asks the
node to provide, such as a permitted clock or HTTP request.
The contract stays the same when you choose another programming language. Generated bindings connect your language's types to that contract. The node checks calls against the contract before running the function.
2. Follow the implementation
Select your language above this complete greeting example:
- Rust
- TypeScript
- Go
- C
- Java
- C#/.NET
pub fn greet(name: String) -> Result<String, String> {
let name = name.trim();
if name.is_empty() {
return Err("Please enter a name.".into());
}
if name.len() > 100 {
return Err("Use a name of at most 100 bytes.".into());
}
Ok(format!("Hello, {name}!"))
}
#[cfg(target_arch = "wasm32")]
mod component {
wit_bindgen::generate!({path: "examples/tutorial_greeting", world: "service"});
struct Capsule;
impl exports::examples::greeting::api::Guest for Capsule {
fn greet(name: String) -> Result<String, String> {
super::greet(name)
}
}
export!(Capsule);
}
export const api: typeof Contract = {
greet(name) {
const clean = trim(name);
if (!clean) throw 'Please enter a name.';
if (utf8Length(clean) > 100) throw 'Use a name of at most 100 bytes.';
return `Hello, ${clean}!`;
},
};
package export_examples_greeting_api
import (
wit "go.bytecodealliance.org/pkg/wit/types"
"strings"
)
func Greet(name string) wit.Result[string, string] {
name = strings.TrimSpace(name)
if len(name) == 0 {
return wit.Err[string, string]("Please enter a name.")
}
if len(name) > 100 {
return wit.Err[string, string]("Use a name of at most 100 bytes.")
}
return wit.Ok[string, string]("Hello, " + name + "!")
}
#include "lsf/text.h"
bool exports_examples_greeting_api_greet(probe_string_t *name, probe_string_t *value,
probe_string_t *error) {
size_t first = name->len, end = 0, offset = 0;
while (offset < name->len) {
size_t start = offset;
if (!lsf_text_whitespace(lsf_text_next(name, &offset))) {
if (first == name->len) first = start;
end = offset;
}
}
const char *failure = first == name->len ? "Please enter a name." :
end - first > 100 ? "Use a name of at most 100 bytes." : NULL;
if (failure) {
probe_string_free(name);
lsf_text_error(error, failure);
return false;
}
lsf_scope_t scope;
lsf_scope_init(&scope, 128);
size_t length = end - first;
uint8_t *bytes = lsf_scope_alloc(&scope, length + 8, 1);
lsf_require(bytes != NULL);
memcpy(bytes, "Hello, ", 7);
memcpy(bytes + 7, name->ptr + first, length);
bytes[length + 7] = '!';
probe_string_free(name);
*value = (probe_string_t){bytes, length + 8};
lsf_string_return(&scope, value);
lsf_scope_close(&scope);
return true;
}
public final class Capsule implements Bindings.Exports {
public Result<String, String> greet(String name) {
String trimmed = Text.trim(name);
if (trimmed.isEmpty()) return Result.err("Please enter a name.");
if (Text.utf8Length(trimmed) > 100) return Result.err("Use a name of at most 100 bytes.");
return Result.ok("Hello, " + trimmed + "!");
}
}
public class ApiExportsImpl : IApiExports
{
public static Result<string, string> Greet(string name)
{
string clean = Text.Trim(name);
if (clean.Length == 0) return Result<string, string>.Err("Please enter a name.");
if (Text.Utf8Length(clean) > 100) return Result<string, string>.Err("Use a name of at most 100 bytes.");
return Result<string, string>.Ok($"Hello, {clean}!");
}
}
The program trims the supplied name, rejects empty or excessive input, and
returns the greeting. Open the matching source under app in the project
created by dev init. Edit that source, then use dev build to compile it.
The edit guide
also shows a Rust watch session that changes Hello, to Welcome, and keeps
the previous deployment working through a compiler error.
This code runs inside the capsule. A client SDK is a separate application that calls the capsule from outside the node. Both offer six language choices, but their libraries have different jobs.
3. Connect the project files
| File or directory | What you use it for |
|---|---|
app | Edit the application source and its WIT contract |
latent.project.json | Review the selected compiler, build recipe and output paths |
tests/scenarios.json | See the named success and failure cases |
tests/*-input.json and tests/*-expected.json | Read or change a case's input and expected typed answer |
.vscode/tasks.json, after dev editor | Run the same commands from an optional editor |
dev build produces the component and its manifests. dev deploy selects that
accepted build for your service. The frontend keeps the generated identifiers
and node credentials; you do not copy them into your source.
Each call starts with fresh guest state. A global variable is not storage for the next request. Host access needs an explicit deployment grant: importing a capability alone does not authorize it. See Use capabilities when your program needs something beyond its input and local computation.
4. Compare a result and an error
The greeting's real-node scenarios include:
| Input | Expected answer | Meaning |
|---|---|---|
Ada | Hello, Ada! | The function returned a successful result |
| An empty name | A declared string error | The program rejected invalid input |
Run dev test --environment node with your workspace as shown in the capsule
walkthrough. Expect every greeting case to pass, including its declared-error
cases. A test passes when the observed outcome matches its expectation; an
expected application error is a useful successful test.
A connection failure, denied capability or exhausted execution budget is a different outcome. Inspect the reported category and original operation status. Do not repeat a call whose response was lost: it may already have executed.
5. Try a different purpose
Creating a capsule
shows how to select the word-count or shipping template. Use a new project
and disposable workspace for each example. Switch language above an example to
compare implementations while keeping the same contract and expected answers.
Use dev down to stop your node while retaining its data. Use the explicit
workspace purge from the developer command guide when the disposable tutorial is finished.
Your application source remains available for the next edit.