Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

Uzumibi lets you write request handlers in Ruby for WebAssembly-based edge and serverless runtimes.

An Uzumibi application has three main parts:

  1. Ruby application code, normally lib/app.rb
  2. The mruby/edge runtime and Uzumibi framework, compiled into a Wasm module
  3. A platform adapter that transfers requests, responses, and optional host services between the platform and Wasm

Ruby code is compiled to mruby bytecode during the application build. It is not loaded from the filesystem at request time.

Minimal application

class App < Uzumibi::Router
  get "/" do |req, res|
    res.return(
      200,
      { "content-type" => "text/plain" },
      "Hello from Uzumibi!\n"
    )
  end
end

$APP = App.new

Uzumibi::Router dispatches by HTTP method and path. A handler receives an Uzumibi::Request and Uzumibi::Response and mutates the response. Ending the handler with res is the conventional style.

What the CLI provides

The uzumibi CLI generates complete, platform-specific projects. The generated build and development commands differ by template; the CLI itself currently only provides the new command.

For Cloudflare Workers, the generated project uses pnpm and Wrangler:

uzumibi new --template cloudflare my-app
cd my-app
pnpm install
pnpm run dev

Continue with Installation and Getting Started, or see the Cloudflare Workers guide.

Overview

This section describes the core concepts behind Uzumibi, including its purpose, the mruby/edge runtime, and the overall architecture.

Sections

What is Uzumibi?

Uzumibi is a small Ruby HTTP framework plus a set of adapters and project templates for edge and serverless platforms.

The core framework provides:

  • routing for GET, POST, PUT, DELETE, HEAD, and OPTIONS
  • named path parameters and wildcard routes
  • request objects containing method, path, headers, parameters, cookies, and body data
  • response objects containing a status code, headers, and a String body
  • a compact binary protocol used by platform adapters to exchange HTTP data with Wasm

Platform-specific functionality is provided by the generated template and adapter crates. It is not guaranteed to be portable between templates. For example, the Cloudflare template can optionally expose Workers KV, Durable Objects, outbound fetch, secrets, Access identity, and Queues.

The name “Uzumibi” (うずみび) refers to live embers kept under ash so that the fire does not go out.

What is mruby/edge?

mruby/edge is the Ruby runtime used by Uzumibi. It implements an mruby-compatible VM in Rust and is designed to run in WebAssembly environments.

In an Uzumibi build:

  1. the project build script compiles lib/app.rb or lib/consumer.rb to mruby bytecode;
  2. that bytecode is embedded in the application binary;
  3. the platform adapter initializes a mruby/edge VM and evaluates the embedded bytecode;
  4. requests or messages are dispatched to the Ruby application.

The available Ruby language and standard-library features are determined by mruby/edge and by the crates initialized by the selected template. Do not assume that every feature or native extension available in CRuby is present.

For runtime implementation details and compatibility information, refer to the mruby/edge repository.

Architecture

Uzumibi separates the Ruby application from the platform-specific host.

HTTP request or platform event
            |
            v
Platform adapter (JavaScript or Rust)
            |
     compact byte buffer
            |
            v
Wasm module
  + mruby/edge VM
  + uzumibi-gem
  + embedded Ruby bytecode
            |
            v
Ruby Router or Queue Consumer

Build time

The generated Rust build script compiles the Ruby source into mruby bytecode and embeds it in the Wasm module or native application. Changing Ruby code therefore requires a rebuild.

Runtime

For HTTP applications, the platform adapter serializes the request method, path, query string, selected headers, and body into a buffer. uzumibi-gem constructs an Uzumibi::Request, dispatches the matching route, and serializes the returned Uzumibi::Response.

The transport is implemented separately for each template:

  • Cloudflare Workers uses a JavaScript Worker around a wasm32-unknown-unknown module.
  • Fastly and Spin run WASI-oriented Rust adapters.
  • Cloud Run runs a native Rust HTTP server.
  • Service Worker and Web Worker templates use browser JavaScript hosts.

Optional host calls

Some operations require calling back from Wasm into the host. On Cloudflare, the enable-external and queue features build the Wasm module with Asyncify so Ruby code can wait for asynchronous Workers APIs such as fetch, KV, and Queues.

These APIs are platform adapter features, not universal uzumibi-gem APIs.

Project Structure

The repository is a Cargo workspace containing the framework, platform adapters, CLI, and integration projects.

PathPurpose
uzumibi-cliCLI and embedded project templates
uzumibi-gemCore Ruby request, response, and router API
uzumibi-art-routerRoute matching implementation
uzumibi-cloudflare-extCloudflare-specific Ruby host APIs
uzumibi-googleGoogle Cloud integrations
uzumibi-docsThis mdBook
uzumibi-on-*-spikePlatform integration and development examples

Generated application layouts are platform-specific. A generated Cloudflare project contains a JavaScript Worker at the root and a Rust Wasm crate under wasm-app; other templates may be a single Rust crate.

Use the generated project’s own scripts and configuration as the source of truth. The spike directories are useful for development, but applications should normally be created with uzumibi new.

Installation and Getting Started

This guide will walk you through installing Uzumibi and creating your first edge application.

Sections

Prerequisites

All generated projects require a current stable Rust toolchain.

For Cloudflare Workers, install:

  • Rust and Cargo
  • the wasm32-unknown-unknown Rust target
  • Node.js
  • pnpm
rustup target add wasm32-unknown-unknown
npm install --global pnpm

The generated project installs Wrangler as a development dependency, so use it through pnpm or pnpm exec.

Projects generated with Cloudflare’s enable-external or queue feature also require Binaryen’s wasm-opt, because those builds apply Asyncify:

brew install binaryen

On other operating systems, install Binaryen using the packages or binaries listed by the Binaryen project.

A Cloudflare account and Wrangler login are required for deployment, but not for creating a project.

Installing via cargo

Install the Uzumibi CLI tool using cargo:

cargo install uzumibi-cli

This will install the uzumibi command-line tool, which you can use to generate new projects.

To verify the installation:

uzumibi --version

Creating a Cloudflare Workers Project

Generate the base HTTP template:

uzumibi new --template cloudflare my-uzumibi-app
cd my-uzumibi-app
pnpm install

The generated project has this structure:

my-uzumibi-app/
├── Cargo.toml
├── package.json
├── pnpm-lock.yaml
├── wrangler.jsonc
├── lib/
│   └── app.rb
├── public/
│   └── assets/
├── scripts/
│   └── build-wasm.mjs
├── src/
│   ├── index.js
│   └── request-buffer.js
├── test/
│   └── request-buffer.spec.js
└── wasm-app/
    ├── Cargo.toml
    ├── build.rs
    └── src/
        └── lib.rs
  • lib/app.rb is the Ruby application.
  • wasm-app/build.rs compiles and embeds the Ruby bytecode.
  • src/index.js is the Workers entry point and Wasm host.
  • scripts/build-wasm.mjs selects the build mode and embeds configuration.
  • wrangler.jsonc configures the Worker and static-assets binding.

Feature variants

Enable asynchronous Cloudflare host APIs:

uzumibi new --template cloudflare --features enable-external my-app

Create a Cloudflare Queues consumer:

uzumibi new --template cloudflare --features queue my-consumer

The Queue variant uses lib/consumer.rb and $CONSUMER instead of lib/app.rb and $APP. The queue feature includes the external-service APIs.

See Cloudflare Workers for configuration and feature details.

Editing Ruby Files

For an HTTP application, edit lib/app.rb:

class App < Uzumibi::Router
  get "/" do |req, res|
    res.return(
      200,
      { "content-type" => "text/plain" },
      "Hello from Uzumibi!\n"
    )
  end

  post "/echo/:name" do |req, res|
    res.status_code = 200
    res.headers = { "content-type" => "text/plain" }
    res.body = "#{req.params[:name]}: #{req.raw_body}\n"
    res
  end
end

$APP = App.new

Each route must set the response status, headers, and body. Ending with res is the conventional style. res.return(status, headers, body) is a convenience method that sets all three fields and returns res.

For a Queue consumer generated with --features queue, edit lib/consumer.rb and keep the generated $CONSUMER global.

Ruby source is compiled to mruby bytecode during the build. The generated development command rebuilds it automatically when the command is restarted.

Running Locally

Cloudflare Workers

From a generated Cloudflare project:

pnpm install
pnpm run dev

pnpm run dev runs the template’s Wasm build script and then starts Wrangler. The default local URL is printed by Wrangler, normally http://localhost:8787.

After changing lib/app.rb, lib/consumer.rb, a Rust dependency, or package.json build configuration, stop and rerun pnpm run dev so the Wasm module is rebuilt.

Use pnpm start only when the Wasm output already exists and you intentionally want to start Wrangler without rebuilding.

The enable-external and queue variants require wasm-opt. They may also require valid KV, Durable Object, or Queue bindings in wrangler.jsonc.

Other templates

Generated commands differ by platform. Run uzumibi new and follow the “Next steps” printed by the CLI, then consult the generated configuration files.

Deploying

Cloudflare Workers

Authenticate Wrangler:

pnpm exec wrangler login

Then build and deploy:

pnpm run deploy

The deploy script selects the correct build mode for the generated template:

  • base template: vanilla Wasm
  • enable-external: Asyncify-enabled Wasm
  • queue: Queue consumer Wasm

Before deploying a feature variant, replace placeholder resource IDs and create any Queue or KV resources referenced by wrangler.jsonc.

Cloudflare account limits and resource configuration can change. Refer to the Cloudflare Workers documentation for platform policy and Wrangler configuration.

Other templates

Deployment commands are platform-specific. Use the next steps printed by the CLI and the configuration generated for that template.

Next Steps

Troubleshooting

Cloudflare Wasm target is missing

rustup target add wasm32-unknown-unknown

wasm-opt is not found

The enable-external and queue variants apply Asyncify and require Binaryen:

brew install binaryen
wasm-opt --version

Ruby changes are not visible

Ruby bytecode is embedded at build time. Stop Wrangler and rerun:

pnpm run dev

Wrangler rejects a placeholder binding

Feature templates include values such as <YOUR_KV_NAMESPACE_ID>. Create or select the Cloudflare resource and replace the placeholder in wrangler.jsonc.

Requests return HTTP 413

The Cloudflare adapter rejects an encoded request larger than uzumibi.httpMaxBytes in package.json. Increase the value and rebuild the Wasm module. The value applies to the complete encoded request, not only the body.

A route returns 404

Check the HTTP method and normalized path. The base router returns 404 Not Found when no route matches. Cloudflare also returns 404 for /favicon.ico before invoking Ruby.

Ruby API Reference

This section describes the Ruby API provided by Uzumibi for building edge applications.

Sections

Routing

Define routes as class methods on a subclass of Uzumibi::Router.

class App < Uzumibi::Router
  get "/items" do |req, res|
    res.return(200, { "content-type" => "text/plain" }, "items\n")
  end

  post "/items" do |req, res|
    res.return(201, { "content-type" => "text/plain" }, "created\n")
  end
end

$APP = App.new

The router supports get, post, put, delete, head, and options.

Named parameters

get "/users/:user_id/posts/:post_id" do |req, res|
  user_id = req.params[:user_id]
  post_id = req.params[:post_id]
  res.return(200, {}, "#{user_id}/#{post_id}")
end

Wildcards

A trailing * captures the remaining path in req.params[:"*"]:

get "/assets/*" do |req, res|
  path = req.params[:"*"]
  res.return(200, {}, path)
end

Query parameters

Query parameters are merged into req.params as Symbol keys:

get "/search" do |req, res|
  query = req.params[:q]
  res.return(200, {}, query || "")
end

The current query parser is intentionally small: it splits & and = pairs and does not URL-decode them. Form-urlencoded request bodies use a separate percent-decoding parser.

HEAD and missing routes

A HEAD request uses the GET router for the same path and clears the response body after the handler runs. If no method/path pair matches, Uzumibi returns status 404 with body Not Found.

Request Object

Route handlers receive an Uzumibi::Request as req.

PropertyValue
req.methodHTTP method String
req.pathRequest pathname
req.headersHeader Hash with String keys and values
req.paramsPath, query, and parsed body parameters with Symbol keys
req.bodyParsed JSON value when supported, otherwise the raw body String
req.raw_bodyRaw request body as a Ruby String
req.cookieParsed Cookie header as a Hash with String keys

Parameters

get "/users/:id" do |req, res|
  id = req.params[:id]
  verbose = req.params[:verbose]
  res.return(200, {}, "#{id}: #{verbose}")
end

Path parameters are merged first, followed by query parameters and then supported body parameters. A later source replaces an earlier value with the same key.

JSON bodies

When the content type is exactly application/json and JSON support is enabled by the template, valid JSON is assigned to req.body. Top-level object fields are also merged into req.params.

post "/users" do |req, res|
  data = req.body
  name = data["name"]
  res.return(
    201,
    { "content-type" => "application/json" },
    JSON.generate({ "created" => name })
  )
end

If parsing fails, req.body remains the raw String. Use req.raw_body when the original payload is required regardless of content type.

Form bodies

For an exact application/x-www-form-urlencoded content type, decoded form fields are merged into req.params.

Headers

Header casing and filtering depend on the platform adapter. The Cloudflare adapter currently passes lowercase Workers header names but omits cf-connecting-ip, cf-ray, and names beginning with x-.

Response Object

Route handlers receive an Uzumibi::Response as res.

Set all three response properties:

get "/" do |req, res|
  res.status_code = 200
  res.headers = { "content-type" => "text/plain" }
  res.body = "Hello\n"
  res
end
PropertyRequired type
res.status_codeInteger representable as an HTTP status
res.headersHash of String-compatible keys and values
res.bodyRuby String

res.return

res.return(status_code, headers, body) assigns all fields and returns the response object:

get "/health" do |req, res|
  res.return(200, { "content-type" => "text/plain" }, "ok\n")
end

The router uses the response object passed to the handler. Ending a handler with res is the conventional style, while res.return is useful for concise handlers.

Encoding

The core response transport serializes the bytes of the Ruby String. Individual platform hosts decide how those bytes become a platform response. The current Cloudflare JavaScript adapter decodes the body as text, so it is not yet a transparent arbitrary-binary response path.

Complete Example

class App < Uzumibi::Router
  get "/" do |req, res|
    res.return(
      200,
      { "content-type" => "text/plain" },
      "Welcome to Uzumibi!\n"
    )
  end

  get "/users/:id" do |req, res|
    res.return(
      200,
      { "content-type" => "application/json" },
      JSON.generate({
        "id" => req.params[:id],
        "verbose" => req.params[:verbose]
      })
    )
  end

  post "/echo" do |req, res|
    res.return(
      200,
      { "content-type" => "application/octet-stream" },
      req.raw_body
    )
  end

  get "/old-path" do |req, res|
    res.return(
      302,
      { "location" => "/", "content-type" => "text/plain" },
      "Moved\n"
    )
  end
end

$APP = App.new

The application/octet-stream example describes the core Ruby response. Check the selected platform adapter before relying on arbitrary binary response bytes; the current Cloudflare host text-decodes response bodies.

Platform Helper Functions

Helper functions are supplied by platform adapters, not by the core uzumibi-gem.

debug_console(message)

The Cloudflare adapter converts the argument with to_s and writes it through the Worker console:

get "/debug" do |req, res|
  debug_console("request path: #{req.path}")
  res.return(200, {}, "logged\n")
end

Other templates can map the same helper to their own logging facility. Logging destination and behavior are platform-specific.

fetch_assets

In a Cloudflare HTTP application, fetch_assets stops Ruby request handling and delegates the original request to the ASSETS binding:

get "/assets/*" do |req, res|
  fetch_assets
end

This helper is Cloudflare-specific.

Error Handling

When no route matches, Uzumibi constructs:

  • status: 404
  • content type: text/plain; charset=utf-8
  • body: Not Found

Handle expected application errors inside the route and set a complete response:

post "/items" do |req, res|
  if req.body.is_a?(Hash) && req.body["name"]
    res.return(
      201,
      { "content-type" => "application/json" },
      JSON.generate({ "created" => req.body["name"] })
    )
  else
    res.return(
      400,
      { "content-type" => "application/json" },
      JSON.generate({ "error" => "name is required" })
    )
  end
end

An unhandled Ruby or adapter error crosses the Wasm boundary as a runtime error. The exact HTTP response and logging behavior then depend on the platform host; Uzumibi does not currently provide a global Ruby error-handler DSL.

Best Practices

  • Set status_code, headers, and body on every successful route, or use res.return.
  • Use req.raw_body when you need the original bytes rather than adapter-assisted parsing.
  • Check the type of req.body before treating it as parsed JSON.
  • Keep platform service calls behind small application methods so platform dependencies stay visible.
  • Restart the generated development command after changing embedded Ruby code.
  • Keep request-size configuration proportional to the platform memory available.
  • Test generated projects with the same feature overlay used in deployment.
  • Treat provider limits and Wrangler configuration as external, versioned dependencies.

Limitations

Current core and adapter constraints include:

  • Ruby code is compiled and embedded at build time.
  • Available Ruby features are those implemented by mruby/edge and initialized crates, not the full CRuby standard library.
  • Native CRuby extensions cannot be loaded into the Wasm runtime.
  • Query parsing does not currently perform URL decoding.
  • JSON and form parsing require exact supported content-type values.
  • Response headers use 16-bit lengths and response bodies use a 32-bit length in the transport format.
  • Platform service APIs are adapter-specific and often require a feature overlay.
  • The Cloudflare adapter has its own configurable encoded-request limit and currently text-decodes response bodies.

See the selected platform guide for build tools, bindings, and host-specific constraints.

Supported Platforms

Uzumibi supports deployment to multiple edge computing platforms. Each platform has its own characteristics, deployment process, and limitations.

Sections

Cloudflare Workers

The Cloudflare template runs an Uzumibi Wasm module inside a JavaScript Worker. The generated project owns both sides of the boundary: Rust and embedded Ruby in wasm-app, and the Workers host in src/index.js.

Create a project

For an HTTP application without asynchronous host calls:

uzumibi new --template cloudflare my-app
cd my-app
pnpm install
pnpm run dev

The generated scripts are:

CommandPurpose
pnpm run devBuild vanilla Wasm and start Wrangler
pnpm run deployBuild vanilla Wasm and deploy
pnpm startStart Wrangler without rebuilding Wasm
pnpm testRun JavaScript tests

Edit lib/app.rb and restart pnpm run dev after a change.

Runtime architecture

For each HTTP request:

  1. src/index.js reads the Workers Request.
  2. src/request-buffer.js encodes the method, pathname, query string, selected headers, and body.
  3. The Rust Wasm export allocates a shared-memory region of the exact encoded size.
  4. uzumibi-gem constructs an Uzumibi::Request and dispatches $APP.
  5. The returned Uzumibi::Response is packed into Wasm memory.
  6. JavaScript reads the status, headers, and body and creates a Workers Response.

The mruby/edge VM is initialized lazily and retained by the Wasm instance.

HTTP request size

The adapter has its own encoded-request limit in addition to Cloudflare’s account and platform limits. The default is 65,536 bytes.

The limit is stored persistently in package.json:

{
  "uzumibi": {
    "httpMaxBytes": 65536
  }
}

The value covers the complete encoded request: framing, method, path, query string, included headers, and body. A request over the configured value receives HTTP 413 before Ruby routing begins.

The build script validates a positive integer up to 2,147,483,647 and embeds it in the Wasm module. Rebuild after changing it:

pnpm run dev

For a one-off build, use either an environment variable or a script option:

UZUMIBI_HTTP_MAX_BYTES=1048576 pnpm run build:wasm:vanilla
node scripts/build-wasm.mjs vanilla --http-max-bytes=1048576

The option has precedence over the environment variable, which has precedence over package.json. Increasing the limit permits a larger allocation; it does not change Cloudflare’s own request or memory limits.

Request and response behavior

  • req.params combines path parameters, query parameters, and supported parsed body parameters.
  • An exact application/json content type parses a JSON object into req.body and merges its top-level fields into req.params.
  • req.raw_body preserves the original request body as a Ruby String.
  • An exact application/x-www-form-urlencoded content type merges form fields into req.params.
  • The current Workers adapter omits cf-connecting-ip, cf-ray, and headers beginning with x- before passing headers to Ruby.
  • A response body is a Ruby String. The current JavaScript adapter decodes it as text when constructing the Workers response; arbitrary binary response bytes are not yet preserved transparently.

Consult Cloudflare Workers limits for current platform limits.

Static assets

The base wrangler.jsonc binds the generated public directory as ASSETS. Call fetch_assets from a route to delegate the original request to env.ASSETS.fetch(request):

get "/assets/*" do |req, res|
  fetch_assets
end

Cloudflare may serve a matching static asset before invoking the Worker depending on the current assets routing configuration. See Workers Static Assets configuration.

External-service feature

Generate an HTTP application with asynchronous Workers APIs:

uzumibi new --template cloudflare --features enable-external my-app

This variant:

  • enables uzumibi-cloudflare-ext/enable-external
  • uses asyncify-wasm at runtime
  • runs wasm-opt --asyncify during the build
  • includes KV and Durable Object binding examples in wrangler.jsonc

Install Binaryen before building:

brew install binaryen

The following Ruby APIs are currently defined:

APIWorkers operation
Uzumibi::Fetch.fetch(url, method = "GET", body = "", headers = {})outbound fetch
Uzumibi::KV.get(key) / .set(key, value)UZUMIBI_KV Workers KV binding
Uzumibi::LegacyKV.get(key) / .set(key, value)generated UzumibiKVObject Durable Object
Uzumibi::Secret.get(name)Worker environment binding with that name
Uzumibi::Queue.send(binding_name, message)Queue producer binding
Uzumibi::Access.team= / .get_identity(token)Cloudflare Access identity endpoint

See Cloudflare Access identity for setup and request handling.

Example outbound request:

response = Uzumibi::Fetch.fetch(
  "https://example.com/api",
  "POST",
  JSON.generate({ "hello" => "world" }),
  { "content-type" => "application/json" }
)

Example KV access:

Uzumibi::KV.set("greeting", "hello")
value = Uzumibi::KV.get("greeting")

Create a KV namespace and replace <YOUR_KV_NAMESPACE_ID> in wrangler.jsonc:

pnpm exec wrangler kv namespace create UZUMIBI_KV

For Queue producers, Uzumibi::Queue.send takes the Wrangler binding name, such as "UZUMIBI_QUEUE", rather than the Cloudflare resource name.

Queue consumer feature

Generate a Queue consumer:

uzumibi new --template cloudflare --features queue my-consumer
cd my-consumer
pnpm install
pnpm exec wrangler queues create my-consumer-queue
pnpm run dev

The generated wrangler.jsonc uses my-consumer-queue as both a producer and consumer resource. The Queue feature includes the external-service feature.

Implement the consumer in lib/consumer.rb:

class Consumer < Uzumibi::Consumer
  def on_receive(message)
    debug_console("received #{message.id}: #{message.body}")

    if message.attempts > 3
      message.ack!
    else
      message.retry(delay_seconds: 3)
    end
  end
end

$CONSUMER = Consumer.new

Uzumibi::Message exposes id, timestamp, body, and attempts, plus ack!, nack!, and retry(delay_seconds: N).

The Queue template is event-oriented. Ordinary HTTP requests are intentionally rejected with HTTP 400.

See the Cloudflare Queues Wrangler commands for current resource-management commands.

Current adapter constraints

  • The base HTTP build cannot call asynchronous external Workers APIs; use enable-external.
  • External fetch and KV reads currently use fixed 64 KiB host-call result buffers.
  • Secret reads currently use an 8 KiB result buffer.
  • Responses are text-decoded by the JavaScript adapter.
  • The Queue consumer processes messages one at a time inside each delivered batch.

These are Uzumibi adapter constraints and are separate from Cloudflare account limits.

Fastly Compute

Fastly Compute@Edge runs WebAssembly workloads at the edge using the WASI interface.

Features

  • Global CDN: Runs on Fastly’s global edge network
  • WASI Support: Standard WebAssembly System Interface
  • High Performance: Near-native execution speed
  • Edge Dictionary: Configuration storage (TBA)
  • KV Store: Key-value storage (TBA)

Project Setup

Generate a new Fastly Compute project:

uzumibi new --template fastly my-app
cd my-app

Configuration

Edit fastly.toml:

name = "my-app"
description = "Uzumibi application on Fastly Compute"
authors = ["Your Name <your.email@example.com>"]
language = "rust"

[local_server]
  [local_server.backends]
    [local_server.backends.backend_name]
      url = "http://httpbin.org"

Local Development

# Build
cargo build --target wasm32-wasi --release

# Run locally
fastly compute serve

Deployment

fastly compute deploy

Limitations

  • Execution Time: Up to 60 seconds
  • Memory: Configurable, typically 128MB-512MB
  • Request Size: 8KB headers, unlimited body
  • Response Size: Unlimited

Platform-Specific Features

  • Access to Fastly KV Store (TBA)
  • Access to Edge Dictionary (TBA)
  • Backend requests configuration (TBA)

Spin (Fermyon Cloud)

Spin is an open-source framework for building and running serverless WebAssembly applications.

Features

  • Component Model: Uses WebAssembly Component Model
  • Open Source: Run anywhere that supports Spin
  • Fermyon Cloud: Managed hosting platform
  • Key-Value Store: Built-in KV storage (TBA)
  • SQLite: Embedded database (TBA)

Project Setup

Generate a new Spin project:

uzumibi new --template spin my-app
cd my-app

Configuration

Edit spin.toml:

spin_manifest_version = 2

[application]
name = "my-app"
version = "0.1.0"
authors = ["Your Name <your.email@example.com>"]

[[trigger.http]]
route = "/..."
component = "my-app"

[component.my-app]
source = "target/wasm32-wasi/release/my_app.wasm"
allowed_outbound_hosts = []
[component.my-app.build]
command = "cargo build --target wasm32-wasi --release"

Local Development

# Build and run
spin build
spin up

Deployment

Deploy to Fermyon Cloud:

spin login
spin deploy

Or run on your own infrastructure using any Spin-compatible runtime.

Limitations

  • Execution Time: Platform-dependent
  • Memory: Platform-dependent
  • Component Model: Uses newer WASI preview 2 (compatibility varies)

Platform-Specific Features

  • Access to Spin KV Store (TBA)
  • Access to SQLite (TBA)
  • Redis integration (TBA)

Cloud Run

Google Cloud Run is a managed compute platform that automatically scales your containers.

Status: Experimental

Features

  • Container-Based: Runs standard OCI containers
  • Auto-Scaling: Scales to zero and up based on traffic
  • HTTP/2: Full HTTP/2 support
  • Long-Running: Supports long execution times
  • Google Cloud Integration: Access to GCP services

Project Setup

Generate a new Cloud Run project:

uzumibi new --template cloudrun my-app
cd my-app

Configuration

The project includes a Dockerfile for containerization:

FROM rust:1.75 as builder
WORKDIR /app
COPY . .
RUN cargo build --release

FROM debian:bookworm-slim
COPY --from=builder /app/target/release/my-app /usr/local/bin/my-app
CMD ["my-app"]

Local Development

# Build and run locally
cargo run

The server will start on http://localhost:8080.

Deployment

# Build container
gcloud builds submit --tag gcr.io/PROJECT_ID/my-app

# Deploy to Cloud Run
gcloud run deploy my-app \
  --image gcr.io/PROJECT_ID/my-app \
  --platform managed \
  --region us-central1 \
  --allow-unauthenticated

Limitations

  • Cold Start: Higher cold start latency compared to edge platforms
  • Cost: Billed per request and compute time
  • Not Edge: Runs in regional data centers, not at the edge

Platform-Specific Features

  • Access to Google Cloud Storage (TBA)
  • Access to Cloud SQL (TBA)
  • Access to Firestore (TBA)

Service Worker/Web Worker (Experimental)

Run Uzumibi directly in the browser using Service Workers or Web Workers.

Status: Experimental - For demonstration and testing purposes

Features

  • Browser-Based: Runs entirely in the browser
  • Offline Support: Service Workers enable offline functionality
  • Client-Side Routing: Handle requests without a server
  • Development Tool: Useful for testing and development

Project Structure

The Service Worker spike project demonstrates:

  • Loading WASM in a Service Worker
  • Intercepting fetch requests
  • Processing requests through Uzumibi
  • Returning responses to the browser

Use Cases

  • Offline-First Apps: Progressive Web Apps with offline routing
  • Development/Testing: Test Uzumibi logic in the browser
  • Client-Side APIs: Mock APIs or client-side data processing
  • Educational: Learn how Uzumibi works

Limitations

  • Browser Only: Not suitable for production server workloads
  • Security Restrictions: Subject to browser security policies
  • Limited Storage: Browser storage APIs only
  • Performance: May be slower than server-side execution

How It Works

  1. Register Service Worker
  2. Service Worker loads WASM module
  3. Intercept fetch events
  4. Route through Uzumibi Router
  5. Return response to page

See the uzumibi-on-serviceworker-spike directory for the complete implementation.

Platform Comparison

This table describes the current Uzumibi templates. It does not attempt to reproduce provider pricing or runtime limits, which change independently of Uzumibi.

TemplateGenerated hostRuby HTTP routingOptional service APIsEvent consumer
cloudflareJavaScript Worker + WasmYesenable-externalCloudflare Queues with queue
cloudrunNative Rust service + DockerYesenable-externalPub/Sub push with queue
fastlyFastly Compute Rust appYesNo feature overlayNo
spinSpin componentYesNo feature overlayNo
serviceworkerBrowser Service Worker + WasmYesNo feature overlayNo
webworkerBrowser Web Worker + WasmYesNo feature overlayNo

“Optional service APIs” means APIs implemented by that template’s adapter. Identical Ruby class names on different platforms can map to different provider services and are not a promise of full portability.

For provider limits and availability, consult the provider’s current documentation.

Choosing a Platform

Choose a template based first on its execution host and the integration you need:

  • Choose cloudflare for a Workers application, Workers KV or Durable Objects access, Cloudflare Access identity, static assets, or a Cloudflare Queues consumer.
  • Choose cloudrun for a containerized Rust service on Google Cloud or its supported Google service integrations.
  • Choose fastly for Fastly Compute.
  • Choose spin for a Spin component.
  • Choose serviceworker or webworker for browser-hosted experiments.

Also check:

  1. whether the template has the required host-service adapter;
  2. whether its Wasm target and build tools fit your environment;
  3. whether the provider’s current limits, regions, and pricing fit the workload;
  4. whether the template is covered by the repository’s integration tests.

The templates are independent adapters. Generating the same Ruby router for another template does not automatically make platform-specific service calls portable.

Platform Service APIs

Uzumibi can expose selected host-platform services to Ruby. These APIs are implemented by platform adapter crates and are available only when the generated template enables the required feature.

They are not currently a complete “write once, run anywhere” abstraction. Check the selected template and feature before using an API.

Sections

How Platform Service APIs Work

WebAssembly code cannot directly call JavaScript promises or provider SDKs. Uzumibi adapter crates define Ruby methods and Wasm imports; the generated host implements those imports with the platform’s native APIs.

For Cloudflare Workers:

Ruby API
   |
uzumibi-cloudflare-ext
   |
Wasm import
   |
generated src/index.js
   |
Workers API or binding

Cloudflare operations such as outbound fetch, KV access, and Queue sends are asynchronous. The enable-external build applies Asyncify with wasm-opt and uses asyncify-wasm so the Ruby call can suspend while JavaScript awaits the Workers API.

The base Cloudflare HTTP template does not enable those asynchronous imports. The queue feature enables the external Rust feature as part of the Queue build.

API names can be shared by another adapter, but their exact provider semantics and configuration may differ. Treat each platform guide as the compatibility contract.

Available Services

Cloudflare Workers

Generate an HTTP application with these APIs using:

uzumibi new --template cloudflare --features enable-external my-app

Outbound HTTP

Uzumibi::Fetch.fetch(url, method = "GET", body = "", headers = {})

Returns an Uzumibi::Response with status_code, headers, and body.

Workers KV

Uzumibi::KV.get(key)          # String or nil
Uzumibi::KV.set(key, value)   # true

The generated host uses the UZUMIBI_KV binding. Only get and set are currently implemented.

Durable Object storage

Uzumibi::LegacyKV.get(key)
Uzumibi::LegacyKV.set(key, value)

The generated project defines a single UzumibiKVObject instance named default. This compatibility API is separate from Workers KV.

Environment bindings and secrets

Uzumibi::Secret.get(name)     # String or nil

The host looks up env[name]. Configure sensitive values with Wrangler secrets rather than committing them.

Queue producer

Uzumibi::Queue.send(binding_name, message)

binding_name is the producer binding in wrangler.jsonc, for example "UZUMIBI_QUEUE". The message is converted to a String.

Cloudflare Access identity

Uzumibi::Access.team = "my-team"
identity = Uzumibi::Access.get_identity(token)

The result is an Uzumibi::AccessIdentity with user_uuid, email, and raw_data.

See Cloudflare Access identity for configuration, a request example, and error-handling guidance.

Static assets

fetch_assets is available in every Cloudflare build. It exits Ruby routing and delegates the original request to the generated ASSETS binding.

Queue consumer

Generate with:

uzumibi new --template cloudflare --features queue my-consumer

The Queue API adds:

  • Uzumibi::Consumer#on_receive(message)
  • Uzumibi::Message#id
  • Uzumibi::Message#timestamp
  • Uzumibi::Message#body
  • Uzumibi::Message#attempts
  • Uzumibi::Message#ack!
  • Uzumibi::Message#nack!
  • Uzumibi::Message#retry(delay_seconds: N)

Not currently implemented by the Cloudflare adapter

The current adapter does not define general-purpose Cache, ObjectStore, or SQL Ruby classes. It also does not implement Workers KV delete, list, or metadata operations.

Cloudflare Access identity

Uzumibi::Access retrieves the identity associated with a user authenticated by Cloudflare Access. It is available only in Cloudflare Workers projects generated with the enable-external feature:

uzumibi new --template cloudflare --features enable-external my-app

This API is for user sessions authenticated through the CF_Authorization cookie. It does not validate Access JWTs locally. Instead, it sends that cookie to Cloudflare’s Access identity endpoint, which returns the full identity payload. See Cloudflare’s application-token documentation for the underlying endpoint and payload.

Configure the team name

Set the Access team name once while the application is loaded. Use the team subdomain only: for https://my-team.cloudflareaccess.com, set "my-team".

class App < Uzumibi::Router
  Uzumibi::Access.team = "my-team"

  # routes ...
end

team= is process-wide configuration for the Wasm instance; do not set it from individual requests.

Retrieve the current user

Read the CF_Authorization cookie from the incoming request and pass it to get_identity.

get "/me" do |req, res|
  token = req.cookie["CF_Authorization"]

  if token.nil? || token.empty?
    res.return(
      401,
      { "content-type" => "application/json" },
      JSON.generate({ "error" => "authentication required" })
    )
  else
    begin
      identity = Uzumibi::Access.get_identity(token)

      res.return(
        200,
        { "content-type" => "application/json" },
        JSON.generate({
          "id" => identity.user_uuid,
          "email" => identity.email
        })
      )
    rescue => error
      debug_console("Cloudflare Access identity lookup failed: #{error.message}")
      res.return(
        401,
        { "content-type" => "application/json" },
        JSON.generate({ "error" => "invalid or expired Access session" })
      )
    end
  end
end

The example assumes that the route is protected by an Access application. Cloudflare normally checks the CF_Authorization cookie before forwarding a protected request; the explicit missing-cookie check also makes the route behave predictably in local development and when its Access policy changes.

Uzumibi::AccessIdentity

get_identity returns an Uzumibi::AccessIdentity object:

PropertyMeaning
user_uuidCloudflare Access user identifier
emailAuthenticated user’s email address
raw_dataComplete identity payload, parsed into Ruby data

Cloudflare may include more fields in its identity payload than Uzumibi exposes as convenience accessors. Use raw_data when you need those fields, and avoid returning it directly to clients because it can contain identity-provider and device information.

Scope and error handling

  • get_identity makes an outbound request, so it requires the enable-external build and its Asyncify toolchain.
  • The current API accepts an Access user-session cookie. It does not accept CF-Access-Client-Id / CF-Access-Client-Secret service-token credentials.
  • A missing, invalid, or expired token is not converted into a special Uzumibi result object. Treat exceptions from get_identity as authentication failure, as in the example above.
  • Only user_uuid and email are copied to dedicated accessors. Read additional claims from raw_data deliberately and validate their presence before using them for authorization.

Feature Support Matrix

This matrix describes the generated Cloudflare variants.

CapabilityBaseenable-externalqueue
HTTP Uzumibi::Router applicationYesYesNo; HTTP returns 400
debug_consoleYesYesYes
fetch_assetsYesYesNot used by the event consumer
Uzumibi::Fetch.fetchNoYesYes
Uzumibi::KV.get/setNoYesYes
Uzumibi::LegacyKV.get/setNoYesYes
Uzumibi::Secret.getNoYesYes
Uzumibi::Queue.sendNoYes, with a producer bindingYes
Uzumibi::Consumer and MessageNoNoYes
Asyncify / wasm-opt requiredNoYesYes

Other platform adapters have their own feature sets. Refer to their generated files and platform guides rather than inferring support from this table.

Cloudflare Usage Examples

These examples require a project generated with --features enable-external unless noted otherwise.

KV-backed counter

class App < Uzumibi::Router
  get "/counter" do |req, res|
    count = (Uzumibi::KV.get("counter") || "0").to_i
    res.return(
      200,
      { "content-type" => "application/json" },
      JSON.generate({ "count" => count })
    )
  end

  post "/counter/increment" do |req, res|
    count = (Uzumibi::KV.get("counter") || "0").to_i + 1
    Uzumibi::KV.set("counter", count.to_s)
    res.return(
      200,
      { "content-type" => "application/json" },
      JSON.generate({ "count" => count })
    )
  end
end

$APP = App.new

Configure UZUMIBI_KV in wrangler.jsonc before running this application.

Outbound JSON request

get "/upstream" do |req, res|
  upstream = Uzumibi::Fetch.fetch(
    "https://example.com/api",
    "GET",
    "",
    { "accept" => "application/json" }
  )

  res.return(
    upstream.status_code,
    { "content-type" => upstream.headers["content-type"] || "text/plain" },
    upstream.body
  )
end

Send a Queue message

After configuring a producer binding named UZUMIBI_QUEUE:

post "/jobs" do |req, res|
  Uzumibi::Queue.send("UZUMIBI_QUEUE", req.raw_body)
  res.return(202, { "content-type" => "text/plain" }, "queued\n")
end

Queue consumer

This example belongs in lib/consumer.rb of a project generated with --features queue:

class Consumer < Uzumibi::Consumer
  def on_receive(message)
    begin
      payload = JSON.parse(message.body)
      debug_console("processing #{payload.inspect}")
      message.ack!
    rescue => error
      debug_console("failed: #{error.message}")
      message.retry(delay_seconds: 10)
    end
  end
end

$CONSUMER = Consumer.new

Contributing Platform Integrations

Platform service APIs span multiple layers. A Cloudflare change may require coordinated updates to:

  • uzumibi-cloudflare-ext for Ruby classes and Wasm imports
  • uzumibi-cli/templates/cloudflare for JavaScript host functions and bindings
  • the enable-external or queue feature overlay
  • unit and runn integration tests
  • this documentation

When adding an API, document the exact Ruby signature, required template feature, binding name, return value, buffer or encoding constraints, and failure behavior. Avoid documenting a planned API as available before its adapter and generated template are both implemented.

Use GitHub issues to discuss API design and compatibility.

CLI Reference

The Uzumibi CLI (uzumibi) is a command-line tool for scaffolding new edge application projects.

Sections

Installation

Install via cargo:

cargo install uzumibi-cli

Verify installation:

uzumibi --version

Commands

The current CLI has one subcommand: uzumibi new.

uzumibi new

uzumibi new [OPTIONS] --template <TEMPLATE> <PROJECT_NAME>

Arguments and options

Argument or optionDescription
<PROJECT_NAME>Project name used in generated files and, by default, as the destination directory
-t, --template <TEMPLATE>Required template name
-d, --dest-dir <DEST_DIR>Write to a directory other than PROJECT_NAME
--forceOverwrite existing files without prompting
--features <FEATURES>Comma-separated feature overlays

Available templates are cloudflare, cloudrun, fastly, spin, serviceworker, and webworker.

Currently defined feature overlays are:

TemplateFeaturePurpose
cloudflareenable-externalAsync Cloudflare host APIs from Ruby
cloudflarequeueCloudflare Queues consumer; includes external APIs
cloudrunenable-externalGoogle Cloud external-service APIs
cloudrunqueuePub/Sub push consumer

Examples

uzumibi new --template cloudflare my-worker
uzumibi new -t cloudflare --features enable-external my-worker
uzumibi new -t cloudflare --features queue queue-consumer
uzumibi new -t cloudflare --dest-dir ./apps/worker my-worker

When files already exist and --force is not supplied, the CLI shows a diff and prompts for each conflicting file.

Help and version

uzumibi --help
uzumibi new --help
uzumibi --version

Project Templates

Each template is a complete platform adapter, not only a deployment configuration.

TemplateHost
cloudflareCloudflare Workers JavaScript host plus a Rust Wasm crate
cloudrunNative Rust HTTP server packaged with Docker
fastlyFastly Compute Rust application
spinSpin component
serviceworkerBrowser Service Worker example
webworkerBrowser Web Worker example

The CLI replaces project-name placeholders while copying the selected template. A feature is an overlay that replaces or adds files after the base template is copied.

Cloudflare build scripts

A generated Cloudflare project provides:

ScriptBehavior
pnpm run devBuild the selected Wasm mode and run Wrangler
pnpm run deployBuild the selected Wasm mode and deploy with Wrangler
pnpm startRun Wrangler without rebuilding
pnpm testRun the JavaScript tests with Vitest

The exact Wasm build script name depends on the selected feature: build:wasm:vanilla, build:wasm:asyncify, or build:wasm:queue.

See Cloudflare Workers for the generated layout and configuration.

Common Workflows

Create a Cloudflare HTTP application

uzumibi new --template cloudflare my-app
cd my-app
pnpm install
pnpm run dev

Edit lib/app.rb, then restart pnpm run dev to rebuild the embedded Ruby bytecode.

Enable Cloudflare host APIs

uzumibi new --template cloudflare --features enable-external my-app
cd my-app
pnpm install

Install wasm-opt, configure the bindings in wrangler.jsonc, then run pnpm run dev.

Create a Cloudflare Queue consumer

uzumibi new --template cloudflare --features queue my-consumer
cd my-consumer
pnpm install
pnpm exec wrangler queues create my-consumer-queue
pnpm run dev

Implement Consumer#on_receive in lib/consumer.rb. Ensure the queue name and bindings in wrangler.jsonc match the resource you created.

Update an existing generated project

Templates are copied at generation time; upgrading uzumibi-cli does not rewrite an existing application. Generate a temporary project with the same template and feature, compare it with your application, and apply the changes you need.

CLI Troubleshooting

uzumibi: command not found

Confirm that Cargo’s binary directory is on PATH:

cargo install uzumibi-cli
uzumibi --version

Template not found

Template names are lowercase:

cloudflare, cloudrun, fastly, spin, serviceworker, webworker

The CLI prints the available template names when a requested template does not exist.

Existing files

The CLI can generate into an existing directory. Without --force, it displays a diff and asks whether to overwrite, skip, or abort for each conflict.

Use --dest-dir to choose a separate destination, or --force only when replacing existing files is intentional.

A feature appears to have no effect

Feature names select template overlay directories. Use only features documented for the selected template; an unknown name does not create an overlay.

Environment Variables

The uzumibi CLI does not define custom environment variables for project generation.

Generated projects may read platform-specific or build-specific variables. The Cloudflare Wasm build recognizes:

VariablePurpose
UZUMIBI_HTTP_MAX_BYTESOne-build override for the maximum encoded HTTP request size
CARGO_TARGET_DIRStandard Cargo target-directory override

For a persistent HTTP-size setting, edit uzumibi.httpMaxBytes in the generated package.json. The precedence is:

  1. --http-max-bytes passed to scripts/build-wasm.mjs
  2. UZUMIBI_HTTP_MAX_BYTES
  3. package.json
  4. the built-in default of 65536

Updating the CLI

Update to the latest version:

cargo install uzumibi-cli --force

Getting Help

  • Run uzumibi --help for command help
  • Visit the GitHub repository
  • Open an issue for bugs or feature requests

Examples

This section provides practical examples of Uzumibi applications for common use cases.

Sections

Basic Examples

Hello World

The simplest Uzumibi application:

class App < Uzumibi::Router
  get "/" do |req, res|
    res.status_code = 200
    res.headers = { "Content-Type" => "text/plain" }
    res.body = "Hello, World!"
    res
  end
end

$APP = App.new

Path Parameters

Extract parameters from the URL path:

class App < Uzumibi::Router
  get "/greet/:name" do |req, res|
    name = req.params[:name]
    
    res.status_code = 200
    res.headers = { "Content-Type" => "text/plain" }
    res.body = "Hello, #{name}!"
    res
  end
  
  get "/users/:user_id/posts/:post_id" do |req, res|
    user_id = req.params[:user_id]
    post_id = req.params[:post_id]
    
    res.status_code = 200
    res.headers = { "Content-Type" => "application/json" }
    res.body = JSON.generate({
      user_id: user_id,
      post_id: post_id
    })
    res
  end
end

$APP = App.new

Query Parameters

Access URL query parameters:

class App < Uzumibi::Router
  get "/search" do |req, res|
    query = req.params[:q] || ""
    page = (req.params[:page] || "1").to_i
    limit = (req.params[:limit] || "10").to_i
    
    res.status_code = 200
    res.headers = { "Content-Type" => "application/json" }
    res.body = JSON.generate({
      query: query,
      page: page,
      limit: limit,
      results: []  # Add your search logic here
    })
    res
  end
end

$APP = App.new

HTTP Methods

GET, POST, PUT, DELETE

Handle different HTTP methods:

class App < Uzumibi::Router
  # GET - Retrieve resource
  get "/users/:id" do |req, res|
    user_id = req.params[:id]
    
    res.status_code = 200
    res.headers = { "Content-Type" => "application/json" }
    res.body = JSON.generate({
      id: user_id,
      name: "User #{user_id}",
      email: "user#{user_id}@example.com"
    })
    res
  end
  
  # POST - Create resource
  post "/users" do |req, res|
    data = JSON.parse(req.raw_body)
    
    res.status_code = 201
    res.headers = { "Content-Type" => "application/json" }
    res.body = JSON.generate({
      id: rand(1000),
      name: data["name"],
      email: data["email"],
      created: true
    })
    res
  end
  
  # PUT - Update resource
  put "/users/:id" do |req, res|
    user_id = req.params[:id]
    data = JSON.parse(req.raw_body)
    
    res.status_code = 200
    res.headers = { "Content-Type" => "application/json" }
    res.body = JSON.generate({
      id: user_id,
      name: data["name"],
      updated: true
    })
    res
  end
  
  # DELETE - Delete resource
  delete "/users/:id" do |req, res|
    user_id = req.params[:id]
    
    res.status_code = 204
    res.body = ""
    res
  end
end

$APP = App.new

JSON API

RESTful API Example

A complete RESTful API example:

class App < Uzumibi::Router
  # List all items
  get "/api/items" do |req, res|
    page = (req.params[:page] || "1").to_i
    
    res.status_code = 200
    res.headers = { "Content-Type" => "application/json" }
    res.body = JSON.generate({
      page: page,
      items: [
        { id: 1, name: "Item 1" },
        { id: 2, name: "Item 2" }
      ]
    })
    res
  end
  
  # Get single item
  get "/api/items/:id" do |req, res|
    item_id = req.params[:id].to_i
    
    res.status_code = 200
    res.headers = { "Content-Type" => "application/json" }
    res.body = JSON.generate({
      id: item_id,
      name: "Item #{item_id}",
      description: "Description for item #{item_id}"
    })
    res
  end
  
  # Create item
  post "/api/items" do |req, res|
    begin
      data = JSON.parse(req.raw_body)
      
      # Validate
      if !data["name"] || data["name"].empty?
        res.status_code = 400
        res.body = JSON.generate({ error: "Name is required" })
      else
        res.status_code = 201
        res.headers = {
          "Content-Type" => "application/json",
          "Location" => "/api/items/#{rand(1000)}"
        }
        res.body = JSON.generate({
          id: rand(1000),
          name: data["name"],
          description: data["description"]
        })
      end
    rescue JSON::ParserError
      res.status_code = 400
      res.body = JSON.generate({ error: "Invalid JSON" })
    end
    res
  end
  
  # Update item
  put "/api/items/:id" do |req, res|
    item_id = req.params[:id].to_i
    
    begin
      data = JSON.parse(req.raw_body)
      
      res.status_code = 200
      res.headers = { "Content-Type" => "application/json" }
      res.body = JSON.generate({
        id: item_id,
        name: data["name"],
        description: data["description"],
        updated: true
      })
    rescue JSON::ParserError
      res.status_code = 400
      res.body = JSON.generate({ error: "Invalid JSON" })
    end
    res
  end
  
  # Delete item
  delete "/api/items/:id" do |req, res|
    res.status_code = 204
    res.body = ""
    res
  end
end

$APP = App.new

Form Handling

Processing Form Data

Handle form submissions:

class App < Uzumibi::Router
  # Show form (HTML)
  get "/form" do |req, res|
    res.status_code = 200
    res.headers = { "Content-Type" => "text/html" }
    res.body = <<~HTML
      <!DOCTYPE html>
      <html>
      <head><title>Form Example</title></head>
      <body>
        <h1>Submit Form</h1>
        <form method="POST" action="/form">
          <label>Name: <input type="text" name="name"></label><br>
          <label>Email: <input type="email" name="email"></label><br>
          <button type="submit">Submit</button>
        </form>
      </body>
      </html>
    HTML
    res
  end
  
  # Process form submission
  post "/form" do |req, res|
    # Form data is automatically parsed into req.params
    # when Content-Type is application/x-www-form-urlencoded
    name = req.params[:name]
    email = req.params[:email]
    
    res.status_code = 200
    res.headers = { "Content-Type" => "text/html" }
    res.body = <<~HTML
      <!DOCTYPE html>
      <html>
      <head><title>Form Submitted</title></head>
      <body>
        <h1>Thank you!</h1>
        <p>Name: #{name}</p>
        <p>Email: #{email}</p>
      </body>
      </html>
    HTML
    res
  end
end

$APP = App.new

Redirects

Redirect Examples

class App < Uzumibi::Router
  # Temporary redirect (302)
  get "/old-path" do |req, res|
    res.status_code = 302
    res.headers = {
      "Location" => "/new-path",
      "Content-Type" => "text/plain"
    }
    res.body = "Redirecting..."
    res
  end
  
  # Permanent redirect (301)
  get "/moved" do |req, res|
    res.status_code = 301
    res.headers = {
      "Location" => "/permanently-moved",
      "Content-Type" => "text/plain"
    }
    res.body = "Moved Permanently"
    res
  end
  
  # Redirect with parameters
  get "/user/:id" do |req, res|
    user_id = req.params[:id]
    res.status_code = 302
    res.headers = { "Location" => "/users/#{user_id}/profile" }
    res.body = ""
    res
  end
end

$APP = App.new

Error Handling

Custom Error Responses

class App < Uzumibi::Router
  get "/error-demo" do |req, res|
    error_type = req.params[:type]
    
    case error_type
    when "404"
      res.status_code = 404
      res.headers = { "Content-Type" => "application/json" }
      res.body = JSON.generate({
        error: "Not Found",
        message: "The requested resource was not found"
      })
    when "500"
      res.status_code = 500
      res.headers = { "Content-Type" => "application/json" }
      res.body = JSON.generate({
        error: "Internal Server Error",
        message: "Something went wrong"
      })
    when "401"
      res.status_code = 401
      res.headers = {
        "Content-Type" => "application/json",
        "WWW-Authenticate" => "Bearer"
      }
      res.body = JSON.generate({
        error: "Unauthorized",
        message: "Authentication required"
      })
    else
      res.status_code = 200
      res.body = "Specify ?type=404|500|401"
    end
    res
  end
  
  # Handle errors in route
  post "/api/data" do |req, res|
    begin
      data = JSON.parse(req.raw_body)
      
      # Process data...
      
      res.status_code = 200
      res.headers = { "Content-Type" => "application/json" }
      res.body = JSON.generate({ success: true })
    rescue JSON::ParserError => e
      res.status_code = 400
      res.headers = { "Content-Type" => "application/json" }
      res.body = JSON.generate({
        error: "Bad Request",
        message: "Invalid JSON: #{e.message}"
      })
    rescue => e
      debug_console("[ERROR] #{e.message}")
      res.status_code = 500
      res.headers = { "Content-Type" => "application/json" }
      res.body = JSON.generate({
        error: "Internal Server Error",
        message: "An unexpected error occurred"
      })
    end
    res
  end
end

$APP = App.new

Headers and Content Types

Working with Headers

class App < Uzumibi::Router
  # Return JSON
  get "/json" do |req, res|
    res.status_code = 200
    res.headers = { "Content-Type" => "application/json" }
    res.body = JSON.generate({ message: "Hello JSON" })
    res
  end
  
  # Return HTML
  get "/html" do |req, res|
    res.status_code = 200
    res.headers = { "Content-Type" => "text/html; charset=utf-8" }
    res.body = "<html><body><h1>Hello HTML</h1></body></html>"
    res
  end
  
  # Return plain text
  get "/text" do |req, res|
    res.status_code = 200
    res.headers = { "Content-Type" => "text/plain; charset=utf-8" }
    res.body = "Hello Plain Text"
    res
  end
  
  # Custom headers
  get "/custom-headers" do |req, res|
    res.status_code = 200
    res.headers = {
      "Content-Type" => "text/plain",
      "X-Custom-Header" => "CustomValue",
      "X-Request-ID" => "#{Time.now.to_i}",
      "Cache-Control" => "public, max-age=3600",
      "X-Powered-By" => "Uzumibi/#{RUBY_VERSION}"
    }
    res.body = "Check the response headers!"
    res
  end
  
  # Read request headers
  get "/echo-headers" do |req, res|
    res.status_code = 200
    res.headers = { "Content-Type" => "application/json" }
    res.body = JSON.generate({
      user_agent: req.headers["user-agent"],
      accept: req.headers["accept"],
      host: req.headers["host"]
    })
    res
  end
end

$APP = App.new

Advanced Patterns

API Versioning

class App < Uzumibi::Router
  # Version 1 API
  get "/api/v1/users" do |req, res|
    res.status_code = 200
    res.headers = { "Content-Type" => "application/json" }
    res.body = JSON.generate({
      version: "1.0",
      users: [{ id: 1, name: "User 1" }]
    })
    res
  end
  
  # Version 2 API (with additional fields)
  get "/api/v2/users" do |req, res|
    res.status_code = 200
    res.headers = { "Content-Type" => "application/json" }
    res.body = JSON.generate({
      version: "2.0",
      users: [{
        id: 1,
        name: "User 1",
        email: "user1@example.com",
        created_at: Time.now.to_i
      }]
    })
    res
  end
end

$APP = App.new

Content Negotiation

class App < Uzumibi::Router
  get "/data" do |req, res|
    accept = req.headers["accept"] || "application/json"
    
    data = { message: "Hello", timestamp: Time.now.to_i }
    
    if accept.include?("application/json")
      res.status_code = 200
      res.headers = { "Content-Type" => "application/json" }
      res.body = JSON.generate(data)
    elsif accept.include?("text/html")
      res.status_code = 200
      res.headers = { "Content-Type" => "text/html" }
      res.body = "<html><body><h1>#{data[:message]}</h1><p>Time: #{data[:timestamp]}</p></body></html>"
    else
      res.status_code = 200
      res.headers = { "Content-Type" => "text/plain" }
      res.body = "Message: #{data[:message]}\nTime: #{data[:timestamp]}"
    end
    res
  end
end

$APP = App.new

Real-World Example

Complete Blog API

A more complete example showing a blog API:

class App < Uzumibi::Router
  # Root endpoint
  get "/" do |req, res|
    res.status_code = 200
    res.headers = { "Content-Type" => "application/json" }
    res.body = JSON.generate({
      name: "Blog API",
      version: "1.0",
      endpoints: {
        posts: "/api/posts",
        authors: "/api/authors"
      }
    })
    res
  end
  
  # List posts
  get "/api/posts" do |req, res|
    page = (req.params[:page] || "1").to_i
    tag = req.params[:tag]
    
    posts = [
      { id: 1, title: "First Post", author_id: 1, tags: ["ruby", "web"] },
      { id: 2, title: "Second Post", author_id: 2, tags: ["edge", "wasm"] }
    ]
    
    # Filter by tag if provided
    posts = posts.select { |p| p[:tags].include?(tag) } if tag
    
    res.status_code = 200
    res.headers = { "Content-Type" => "application/json" }
    res.body = JSON.generate({
      page: page,
      posts: posts
    })
    res
  end
  
  # Get single post
  get "/api/posts/:id" do |req, res|
    post_id = req.params[:id].to_i
    
    res.status_code = 200
    res.headers = { "Content-Type" => "application/json" }
    res.body = JSON.generate({
      id: post_id,
      title: "Post #{post_id}",
      content: "Content for post #{post_id}",
      author_id: 1,
      created_at: Time.now.to_i
    })
    res
  end
  
  # Create post
  post "/api/posts" do |req, res|
    begin
      data = JSON.parse(req.raw_body)
      
      if !data["title"] || data["title"].empty?
        res.status_code = 400
        res.body = JSON.generate({ error: "Title is required" })
      else
        new_id = rand(1000)
        res.status_code = 201
        res.headers = {
          "Content-Type" => "application/json",
          "Location" => "/api/posts/#{new_id}"
        }
        res.body = JSON.generate({
          id: new_id,
          title: data["title"],
          content: data["content"],
          author_id: data["author_id"],
          created_at: Time.now.to_i
        })
      end
    rescue JSON::ParserError
      res.status_code = 400
      res.body = JSON.generate({ error: "Invalid JSON" })
    end
    res
  end
  
  # List authors
  get "/api/authors" do |req, res|
    res.status_code = 200
    res.headers = { "Content-Type" => "application/json" }
    res.body = JSON.generate({
      authors: [
        { id: 1, name: "Alice", email: "alice@example.com" },
        { id: 2, name: "Bob", email: "bob@example.com" }
      ]
    })
    res
  end
  
  # Get single author
  get "/api/authors/:id" do |req, res|
    author_id = req.params[:id].to_i
    
    res.status_code = 200
    res.headers = { "Content-Type" => "application/json" }
    res.body = JSON.generate({
      id: author_id,
      name: "Author #{author_id}",
      email: "author#{author_id}@example.com",
      bio: "Biography for author #{author_id}"
    })
    res
  end
end

$APP = App.new