Introduction
Uzumibi lets you write request handlers in Ruby for WebAssembly-based edge and serverless runtimes.
An Uzumibi application has three main parts:
- Ruby application code, normally
lib/app.rb - The mruby/edge runtime and Uzumibi framework, compiled into a Wasm module
- 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, andOPTIONS - 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:
- the project build script compiles
lib/app.rborlib/consumer.rbto mruby bytecode; - that bytecode is embedded in the application binary;
- the platform adapter initializes a mruby/edge VM and evaluates the embedded bytecode;
- 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-unknownmodule. - 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.
| Path | Purpose |
|---|---|
uzumibi-cli | CLI and embedded project templates |
uzumibi-gem | Core Ruby request, response, and router API |
uzumibi-art-router | Route matching implementation |
uzumibi-cloudflare-ext | Cloudflare-specific Ruby host APIs |
uzumibi-google | Google Cloud integrations |
uzumibi-docs | This mdBook |
uzumibi-on-*-spike | Platform 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
- Installing via cargo
- Creating a Cloudflare Workers Project
- Editing Ruby Files
- Running Locally
- Deploying
- Troubleshooting
- Next Steps
Prerequisites
All generated projects require a current stable Rust toolchain.
For Cloudflare Workers, install:
- Rust and Cargo
- the
wasm32-unknown-unknownRust 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.rbis the Ruby application.wasm-app/build.rscompiles and embeds the Ruby bytecode.src/index.jsis the Workers entry point and Wasm host.scripts/build-wasm.mjsselects the build mode and embeds configuration.wrangler.jsoncconfigures 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 Wasmqueue: 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
- Learn about the Ruby API for routing and request/response handling
- Explore supported platforms and platform-specific features
- Check platform service APIs for adapter-specific integrations
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
- Request Object
- Response Object
- Complete Example
- Helper Functions
- Error Handling
- Best Practices
- Limitations
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.
| Property | Value |
|---|---|
req.method | HTTP method String |
req.path | Request pathname |
req.headers | Header Hash with String keys and values |
req.params | Path, query, and parsed body parameters with Symbol keys |
req.body | Parsed JSON value when supported, otherwise the raw body String |
req.raw_body | Raw request body as a Ruby String |
req.cookie | Parsed 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
| Property | Required type |
|---|---|
res.status_code | Integer representable as an HTTP status |
res.headers | Hash of String-compatible keys and values |
res.body | Ruby 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, andbodyon every successful route, or useres.return. - Use
req.raw_bodywhen you need the original bytes rather than adapter-assisted parsing. - Check the type of
req.bodybefore 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
- Fastly Compute
- Spin (Fermyon Cloud)
- Cloud Run
- Service Worker/Web Worker (Experimental)
- Platform Comparison
- Choosing a Platform
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:
| Command | Purpose |
|---|---|
pnpm run dev | Build vanilla Wasm and start Wrangler |
pnpm run deploy | Build vanilla Wasm and deploy |
pnpm start | Start Wrangler without rebuilding Wasm |
pnpm test | Run JavaScript tests |
Edit lib/app.rb and restart pnpm run dev after a change.
Runtime architecture
For each HTTP request:
src/index.jsreads the WorkersRequest.src/request-buffer.jsencodes the method, pathname, query string, selected headers, and body.- The Rust Wasm export allocates a shared-memory region of the exact encoded size.
uzumibi-gemconstructs anUzumibi::Requestand dispatches$APP.- The returned
Uzumibi::Responseis packed into Wasm memory. - 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.paramscombines path parameters, query parameters, and supported parsed body parameters.- An exact
application/jsoncontent type parses a JSON object intoreq.bodyand merges its top-level fields intoreq.params. req.raw_bodypreserves the original request body as a Ruby String.- An exact
application/x-www-form-urlencodedcontent type merges form fields intoreq.params. - The current Workers adapter omits
cf-connecting-ip,cf-ray, and headers beginning withx-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-wasmat runtime - runs
wasm-opt --asyncifyduring 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:
| API | Workers 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
- Register Service Worker
- Service Worker loads WASM module
- Intercept fetch events
- Route through Uzumibi Router
- 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.
| Template | Generated host | Ruby HTTP routing | Optional service APIs | Event consumer |
|---|---|---|---|---|
cloudflare | JavaScript Worker + Wasm | Yes | enable-external | Cloudflare Queues with queue |
cloudrun | Native Rust service + Docker | Yes | enable-external | Pub/Sub push with queue |
fastly | Fastly Compute Rust app | Yes | No feature overlay | No |
spin | Spin component | Yes | No feature overlay | No |
serviceworker | Browser Service Worker + Wasm | Yes | No feature overlay | No |
webworker | Browser Web Worker + Wasm | Yes | No feature overlay | No |
“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
cloudflarefor a Workers application, Workers KV or Durable Objects access, Cloudflare Access identity, static assets, or a Cloudflare Queues consumer. - Choose
cloudrunfor a containerized Rust service on Google Cloud or its supported Google service integrations. - Choose
fastlyfor Fastly Compute. - Choose
spinfor a Spin component. - Choose
serviceworkerorwebworkerfor browser-hosted experiments.
Also check:
- whether the template has the required host-service adapter;
- whether its Wasm target and build tools fit your environment;
- whether the provider’s current limits, regions, and pricing fit the workload;
- 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
- Available Services
- Feature Support Matrix
- Cloudflare Usage Examples
- Contributing
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#idUzumibi::Message#timestampUzumibi::Message#bodyUzumibi::Message#attemptsUzumibi::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:
| Property | Meaning |
|---|---|
user_uuid | Cloudflare Access user identifier |
email | Authenticated user’s email address |
raw_data | Complete 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_identitymakes an outbound request, so it requires theenable-externalbuild and its Asyncify toolchain.- The current API accepts an Access user-session cookie. It does not accept
CF-Access-Client-Id/CF-Access-Client-Secretservice-token credentials. - A missing, invalid, or expired token is not converted into a special Uzumibi result object. Treat exceptions from
get_identityas authentication failure, as in the example above. - Only
user_uuidandemailare copied to dedicated accessors. Read additional claims fromraw_datadeliberately and validate their presence before using them for authorization.
Feature Support Matrix
This matrix describes the generated Cloudflare variants.
| Capability | Base | enable-external | queue |
|---|---|---|---|
HTTP Uzumibi::Router application | Yes | Yes | No; HTTP returns 400 |
debug_console | Yes | Yes | Yes |
fetch_assets | Yes | Yes | Not used by the event consumer |
Uzumibi::Fetch.fetch | No | Yes | Yes |
Uzumibi::KV.get/set | No | Yes | Yes |
Uzumibi::LegacyKV.get/set | No | Yes | Yes |
Uzumibi::Secret.get | No | Yes | Yes |
Uzumibi::Queue.send | No | Yes, with a producer binding | Yes |
Uzumibi::Consumer and Message | No | No | Yes |
Asyncify / wasm-opt required | No | Yes | Yes |
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-extfor Ruby classes and Wasm importsuzumibi-cli/templates/cloudflarefor JavaScript host functions and bindings- the
enable-externalorqueuefeature 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
- Commands
- Project Templates
- Common Workflows
- Troubleshooting
- Environment Variables
- Updating the CLI
- Getting Help
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 option | Description |
|---|---|
<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 |
--force | Overwrite 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:
| Template | Feature | Purpose |
|---|---|---|
cloudflare | enable-external | Async Cloudflare host APIs from Ruby |
cloudflare | queue | Cloudflare Queues consumer; includes external APIs |
cloudrun | enable-external | Google Cloud external-service APIs |
cloudrun | queue | Pub/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.
| Template | Host |
|---|---|
cloudflare | Cloudflare Workers JavaScript host plus a Rust Wasm crate |
cloudrun | Native Rust HTTP server packaged with Docker |
fastly | Fastly Compute Rust application |
spin | Spin component |
serviceworker | Browser Service Worker example |
webworker | Browser 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:
| Script | Behavior |
|---|---|
pnpm run dev | Build the selected Wasm mode and run Wrangler |
pnpm run deploy | Build the selected Wasm mode and deploy with Wrangler |
pnpm start | Run Wrangler without rebuilding |
pnpm test | Run 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:
| Variable | Purpose |
|---|---|
UZUMIBI_HTTP_MAX_BYTES | One-build override for the maximum encoded HTTP request size |
CARGO_TARGET_DIR | Standard Cargo target-directory override |
For a persistent HTTP-size setting, edit uzumibi.httpMaxBytes in the generated package.json. The precedence is:
--http-max-bytespassed toscripts/build-wasm.mjsUZUMIBI_HTTP_MAX_BYTESpackage.json- the built-in default of 65536
Updating the CLI
Update to the latest version:
cargo install uzumibi-cli --force
Getting Help
- Run
uzumibi --helpfor 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
- HTTP Methods
- JSON API
- Form Handling
- Redirects
- Error Handling
- Headers and Content Types
- Advanced Patterns
- Real-World Example
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