Wado

A Tour of Wado Through an HTTP Service

Feature lists tell you what a language has; a real program tells you what it's like to use. So let's read one. example/http_bin.wado is a single-file clone of httpbin — the echo service every HTTP developer has curl'd at some point. It answers /get, /headers, /status/:code, and more on the Component Model's wasi:http/service world, leaning on the standard library the whole way.

Run it and hit an endpoint:

$ wado serve example/http_bin.wado
HTTP server listening on http://0.0.0.0:8080/

$ curl -i http://localhost:8080/get
HTTP/1.1 200 OK
server: http_bin.wado
content-type: application/json
trailer: server-timing
transfer-encoding: chunked

{
  "method": "GET",
  "path": "/get",
  "args": {},
  "headers": {
    "host": "localhost:8080",
    "user-agent": "curl/8.5.0",
    "accept": "*/*"
  },
  "origin": "",
  "url": "http://localhost:8080/get",
  "body": null
}

That trailer: server-timing is real — the handler sets it after the body, and curl delivers it. And the same handler already speaks HTTP/2: curl --http2-prior-knowledge gets an HTTP/2 200, because wasmtime terminates the protocol and wasi:http is deliberately protocol-agnostic.

We'll walk through it, from the shape of the service down to the language details.

The shape of a service

One-file scripts, with the standard library and WASI in scope

A Wado file can be made directly executable with a shebang — one-file scripts are first-class:

#!/usr/bin/env wado serve

The standard library is namespaced (core:* for the language runtime, wasi:* for WASI worlds) and imported the way ES modules are:

use { Request, Response, Method, Scheme, ErrorCode } from "wasi:http";
use { MonotonicClock } from "wasi:clocks";
use { TreeMap } from "core:collections";
use { decode } from "core:base64";
use { Serialize } from "core:serde";
use { Url } from "core:url";
use json from "core:json";

Notice that wasi:http and wasi:clocks are imported the same way as core:json — WASI worlds are simply modules.

Effects belong in the function type

export async fn handle(request: Request) -> Result<Response, ErrorCode>
    with MonotonicClock {

The with clause lists the capabilities this function uses. MonotonicClock is a WASI capability, not a global; the runtime grants it on entry. Pure functions have no with clause, and you can tell at a glance.

async fn declares a Component Model–level async function, which the host can suspend and resume across the canonical ABI.

task return: hand off the response, then keep working

task return Result::Ok(response);

body_tx.write(body_str.as_bytes());
body_tx.drop();

In a Component Model async fn, task return delivers the result to the caller without ending the function. The handler can then continue streaming the body, write trailers, and emit access logs — all after the response status is already on the wire. That server-timing trailer from earlier is written right here.

Handling a request

match as an expression, even on strings

match path {
    "/"            => { content_type = "text/html; charset=utf-8"; body_str = HOMEPAGE; },
    "/get"         => { /* ... */ },
    "/post"        => { /* ... */ },
    "/headers"     => { /* ... */ },
    "/ip"          => { /* ... */ },
    "/user-agent"  => { /* ... */ },
    _              => { /* dynamic routes: /status/:code, /base64/:val, ... */ },
}

match is an expression and works on string literals as well as variants. The wildcard arm dispatches the dynamic routes through a separate function.

Tuples are first-class return values

fn echo_if_method(/* ... */) -> [StatusCode, String, Option<String>] {
    /* ... */
    return [200, build_request_json(/* ... */), null];
}

let [s, b, a] = echo_if_method(request, method, Method::Get, "GET", false, url, path, args);

Tuple literal syntax ([a, b, c]) is the same in types, expressions, and patterns. There is no third syntax for "multiple return values" — there's just tuples and destructuring.

Auto-derived JSON, one line

struct RequestInfo {
    method: String,
    path: String,
    args: TreeMap<String, String>,
    headers: TreeMap<String, String>,
    origin: String,
    url: String,
    data: Option<String>,
}
impl Serialize for RequestInfo;

The single line impl Serialize for RequestInfo; derives the serializer. There is no separate macro and no schema file — the struct's field types are the schema.

Declarative naming for the wire format

#[serde(rename_all = "kebab-case")]
struct UserAgentResponse {
    user_agent: Option<String>,
}
impl Serialize for UserAgentResponse;

The attribute renames user_agent to user-agent on the wire. Idiomatic Wado naming on the inside, idiomatic JSON on the outside.

The language up close

Exhaustive pattern matching over variants

fn scheme_name(s: Option<Scheme>) -> String {
    return match s {
        Some(Http)        => "http",
        Some(Https)       => "https",
        Some(Other(name)) => name,
        None              => "http",
    };
}

Scheme is a variant — a sum type with payloads — imported straight from wasi:http. The compiler verifies that every case is covered, including the nested Other(name) payload that carries a custom scheme string.

Template strings with format specifiers

trailers.append(
    "server-timing",
    `app;dur={elapsed_ms:0.3f}`.as_bytes() as FieldValue,
);

Backtick strings interpolate expressions like TypeScript, but the :0.3f format specifier is checked at compile time against the value's Display impl. The result is app;dur=12.345.

if let with guards

if let Some(a) = request.get_authority() && !a.is_empty() {
    return a;
}

Pattern binding and a boolean guard combine in a single condition. The bound name a is in scope for both the guard and the body.

Compile-time file inclusion

global HOMEPAGE: String = #include_str("./http-bin-index.html");

The HTML for / lives in a separate file for editing comfort, but ends up baked into the .wasm as a constant. There is no filesystem read at runtime, and no separate asset to ship.

Read the whole thing

None of these is a headline feature on its own. Together they're the texture of the language: effects in the type, errors as values, one obvious syntax per idea, and the Component Model showing through instead of being hidden. The full source is example/http_bin.wado — a complete, working httpbin in a single file.