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

Welcome to the Uzumibi documentation!

Uzumibi is a lightweight web application framework for embedding MRuby into edge computing platforms like Cloudflare Workers, Fastly Compute@Edge, Spin, and more. It allows developers to write serverless applications using Ruby, leveraging the power of MRuby for efficient execution in constrained environments.

Quick Example

Here’s a simple example of an Uzumibi application:

class App < Uzumibi::Router
  get "/" do |req, res|
    res.status_code = 200
    res.headers = {
      "content-type" => "text/plain",
      "x-powered-by" => "#{RUBY_ENGINE} #{RUBY_VERSION}"
    }
    res.body = "Hello from Uzumibi!"
    res
  end

  get "/greet/:name" do |req, res|
    res.status_code = 200
    res.headers = { "content-type" => "text/plain" }
    res.body = "Hello, #{req.params[:name]}!"
    res
  end
end

$APP = App.new

Why Uzumibi?

  • Ruby on the Edge: Write edge functions in Ruby instead of JavaScript
  • Lightweight: Built on mruby/edge, optimized for WebAssembly and constrained environments
  • Multi-platform: Deploy to Cloudflare Workers, Fastly Compute, Spin, and more
  • Simple API: Familiar Sinatra-like routing DSL

Get Started

Head over to the Installation and Getting Started guide to begin building with Uzumibi!

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 lightweight web application framework designed for edge computing platforms. The name “Uzumibi” (うずみび) is a Japanese term that refers to live embers buried under a layer of ash to keep the fire from going out. This metaphor represents how Uzumibi keeps Ruby alive in the constrained environments of edge computing.

The framework enables developers to:

  • Write serverless applications in Ruby for edge platforms
  • Deploy to multiple edge providers (Cloudflare Workers, Fastly Compute, Spin, etc.)
  • Build high-performance applications optimized for WebAssembly
  • Use a familiar Sinatra-like routing DSL

What is mruby/edge?

mruby/edge is a specialized implementation of mruby, optimized specifically for edge computing scenarios. It’s designed to run efficiently in WebAssembly environments with limited resources.

Key features of mruby/edge:

  • Optimized for WebAssembly: Compiled to WASM for fast startup and low memory footprint
  • Minimal Runtime: Stripped-down Ruby implementation suitable for edge environments
  • No GC Overhead: Carefully managed memory allocation for predictable performance
  • Edge-Optimized: Built specifically for constrained computing environments

Architecture

Uzumibi’s architecture consists of several key components:

Core Components

┌─────────────────────────────────────────┐
│         Edge Platform                   │
│  (Cloudflare Workers, Fastly, Spin)    │
└─────────────┬───────────────────────────┘
              │
              │ HTTP Request
              ▼
┌─────────────────────────────────────────┐
│     Platform-Specific Runtime           │
│  (WASM Host Environment)                │
└─────────────┬───────────────────────────┘
              │
              ▼
┌─────────────────────────────────────────┐
│      Uzumibi WASM Module                │
│  ┌───────────────────────────────────┐  │
│  │    mruby/edge Runtime             │  │
│  │  ┌─────────────────────────────┐  │  │
│  │  │   Your Ruby Application     │  │  │
│  │  │   (Uzumibi::Router)         │  │  │
│  │  └─────────────────────────────┘  │  │
│  └───────────────────────────────────┘  │
└─────────────┬───────────────────────────┘
              │
              │ HTTP Response
              ▼

Component Layers

  1. uzumibi-cli: Command-line tool for generating project scaffolds
  2. uzumibi-gem: Core framework providing the Router class and request/response handling
  3. uzumibi-art-router: Lightweight router library for path matching and parameter extraction
  4. mruby/edge: Ruby runtime optimized for edge computing
  5. Platform Adapters: Platform-specific code for each edge provider

Request Flow

  1. HTTP request arrives at the edge platform
  2. Platform routes to WASM module
  3. Request data is serialized and passed to mruby/edge
  4. Your Router class processes the request
  5. Response is generated and serialized
  6. Platform sends HTTP response to client

Project Structure

The Uzumibi project consists of several crates:

  • uzumibi-cli: CLI tool for project generation
  • uzumibi-gem: Core framework functionality
  • uzumibi-art-router: Routing library
  • uzumibi-on-*-spike: Example implementations for each platform

Each spike project demonstrates how to integrate Uzumibi with a specific edge platform.

Installation and Getting Started

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

Sections

Prerequisites

Before you begin, make sure you have:

  • Rust toolchain (1.70 or later)
  • wasm32-unknown-unknown target installed
  • Platform-specific tools (e.g., wrangler for Cloudflare Workers)

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

Let’s create a new Uzumibi application for Cloudflare Workers:

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

This generates a new project with the following structure:

my-uzumibi-app/
├── Cargo.toml
├── build.rs
├── wrangler.jsonc
├── lib/
│   └── app.rb          # Your Ruby application
├── src/
│   └── index.js        # JavaScript entry point
└── wasm-app/
    ├── Cargo.toml
    └── src/
        └── lib.rs      # Rust WASM module

Available Templates

The CLI supports the following templates:

  • cloudflare: Cloudflare Workers
  • fastly: Fastly Compute@Edge
  • spin: Spin (Fermyon Cloud)
  • cloudrun: Google Cloud Run (experimental)

Editing Ruby Files

Open lib/app.rb and define your routes:

class App < Uzumibi::Router
  get "/" do |req, res|
    res.status_code = 200
    res.headers = {
      "Content-Type" => "text/plain",
      "X-Powered-By" => "#{RUBY_ENGINE} #{RUBY_VERSION}"
    }
    res.body = "Hello from Uzumibi on the edge!\n"
    res
  end

  get "/hello/:name" do |req, res|
    res.status_code = 200
    res.headers = { "Content-Type" => "text/plain" }
    res.body = "Hello, #{req.params[:name]}!\n"
    res
  end

  post "/data" do |req, res|
    res.status_code = 200
    res.headers = { "Content-Type" => "application/json" }
    res.body = JSON.generate({ received: req.body })
    res
  end
end

$APP = App.new

The Ruby code is compiled to mruby bytecode during the build process and embedded into the WASM module.

Running Locally

Cloudflare Workers

First, build the WASM module:

cd wasm-app
cargo build --target wasm32-unknown-unknown --release
cd ..

Copy the WASM file to the appropriate location:

cp target/wasm32-unknown-unknown/release/uzumibi_cloudflare_app.wasm public/app.wasm

Then start the development server:

npx wrangler dev

Your application will be available at http://localhost:8787.

Fastly Compute

Build the project:

cargo build --target wasm32-wasi --release

Run locally:

fastly compute serve

Spin

Build and run:

spin build
spin up

Deploying

Cloudflare Workers

Make sure you have a Cloudflare account and wrangler configured:

npx wrangler login

Deploy your application:

npx wrangler deploy

Fastly Compute

Deploy to Fastly:

fastly compute deploy

Spin (Fermyon Cloud)

Deploy to Fermyon Cloud:

spin deploy

Next Steps

Troubleshooting

Build Errors

If you encounter build errors, make sure:

  1. The wasm32-unknown-unknown target is installed:

    rustup target add wasm32-unknown-unknown
    
  2. For Fastly, install the wasm32-wasi target:

    rustup target add wasm32-wasi
    

Ruby Code Not Updating

The Ruby code is compiled at build time. After changing lib/app.rb, you need to rebuild the WASM module:

cargo build --target wasm32-unknown-unknown --release

WASM Module Too Large

To reduce WASM module size:

  1. Use release builds (already configured)
  2. Strip debug symbols (already configured)
  3. Minimize Ruby code and dependencies

Ruby API Reference

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

Sections

Routing

Uzumibi provides a Sinatra-inspired routing DSL. Your application class should inherit from Uzumibi::Router.

Defining Routes

Routes are defined using HTTP method names as class methods:

class App < Uzumibi::Router
  get "/path" do |req, res|
    # Handle GET request
  end

  post "/path" do |req, res|
    # Handle POST request
  end

  put "/path" do |req, res|
    # Handle PUT request
  end

  delete "/path" do |req, res|
    # Handle DELETE request
  end

  head "/path" do |req, res|
    # Handle HEAD request (body will be automatically cleared)
  end
end

Path Parameters

You can define dynamic path segments using the :param syntax:

class App < Uzumibi::Router
  get "/users/:id" do |req, res|
    user_id = req.params[:id]
    res.status_code = 200
    res.headers = { "Content-Type" => "text/plain" }
    res.body = "User ID: #{user_id}"
    res
  end

  get "/posts/:post_id/comments/:comment_id" do |req, res|
    post_id = req.params[:post_id]
    comment_id = req.params[:comment_id]
    res.status_code = 200
    res.body = "Post: #{post_id}, Comment: #{comment_id}"
    res
  end
end

Query Parameters

Query parameters are automatically parsed and available in req.params:

get "/search" do |req, res|
  query = req.params[:q]
  page = req.params[:page] || "1"
  
  res.status_code = 200
  res.body = "Search: #{query}, Page: #{page}"
  res
end

For a request to /search?q=ruby&page=2, req.params will contain both :q and :page.

Request Object

The request object (req) provides access to all request data.

Properties

req.method

The HTTP method as a string:

get "/debug" do |req, res|
  res.body = "Method: #{req.method}"  # => "GET"
  res
end

req.path

The request path:

get "/users/:id" do |req, res|
  res.body = "Path: #{req.path}"  # => "/users/123"
  res
end

req.headers

A hash of request headers (keys are lowercase):

get "/" do |req, res|
  user_agent = req.headers["user-agent"]
  content_type = req.headers["content-type"]
  
  res.body = "UA: #{user_agent}"
  res
end

req.params

A hash containing both path parameters and query parameters:

get "/greet/:name" do |req, res|
  # For request: /greet/alice?title=Dr
  name = req.params[:name]    # => "alice"
  title = req.params[:title]  # => "Dr"
  
  res.body = "Hello, #{title} #{name}!"
  res
end

req.body

The request body as a string:

post "/data" do |req, res|
  data = req.body
  res.status_code = 200
  res.body = "Received: #{data}"
  res
end

For application/json content type, you can parse it:

post "/api/users" do |req, res|
  begin
    data = JSON.parse(req.body)
    name = data["name"]
    
    res.status_code = 201
    res.headers = { "Content-Type" => "application/json" }
    res.body = JSON.generate({ created: name })
  rescue => e
    res.status_code = 400
    res.body = "Invalid JSON"
  end
  res
end

For application/x-www-form-urlencoded content type, form data is automatically parsed into req.params.

Response Object

The response object (res) is used to build the HTTP response.

Properties

res.status_code

Set the HTTP status code:

get "/ok" do |req, res|
  res.status_code = 200
  res
end

get "/not-found" do |req, res|
  res.status_code = 404
  res.body = "Not Found"
  res
end

post "/created" do |req, res|
  res.status_code = 201
  res.body = "Created"
  res
end

Common status codes:

  • 200 - OK
  • 201 - Created
  • 204 - No Content
  • 301 - Moved Permanently
  • 302 - Found (Redirect)
  • 400 - Bad Request
  • 401 - Unauthorized
  • 403 - Forbidden
  • 404 - Not Found
  • 500 - Internal Server Error

res.headers

Set response headers as a hash:

get "/" do |req, res|
  res.status_code = 200
  res.headers = {
    "Content-Type" => "text/html",
    "X-Custom-Header" => "value",
    "Cache-Control" => "public, max-age=3600"
  }
  res.body = "<h1>Hello</h1>"
  res
end

res.body

Set the response body as a string:

get "/json" do |req, res|
  res.status_code = 200
  res.headers = { "Content-Type" => "application/json" }
  res.body = JSON.generate({
    message: "Hello",
    timestamp: Time.now.to_i
  })
  res
end

Returning the Response

Always return the res object from the route handler:

get "/example" do |req, res|
  res.status_code = 200
  res.body = "Example"
  res  # Important: return the response object
end

Complete Example

Here’s a complete example demonstrating the API:

class App < Uzumibi::Router
  # Simple GET route
  get "/" do |req, res|
    res.status_code = 200
    res.headers = {
      "Content-Type" => "text/plain",
      "X-Powered-By" => "Uzumibi"
    }
    res.body = "Welcome to Uzumibi!"
    res
  end

  # Path parameters
  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}" })
    res
  end

  # Query parameters
  get "/search" do |req, res|
    query = req.params[:q] || ""
    res.status_code = 200
    res.body = "Searching for: #{query}"
    res
  end

  # POST with body
  post "/api/data" do |req, res|
    debug_console("[Uzumibi] Received: #{req.body}")
    
    res.status_code = 200
    res.headers = { "Content-Type" => "text/plain" }
    res.body = "Received #{req.body.size} bytes"
    res
  end

  # Redirect
  get "/old-path" do |req, res|
    res.status_code = 301
    res.headers = { "Location" => "/new-path" }
    res.body = "Moved"
    res
  end

  # Error response
  get "/error" do |req, res|
    res.status_code = 500
    res.headers = { "Content-Type" => "text/plain" }
    res.body = "Internal Server Error"
    res
  end
end

$APP = App.new

Helper Functions

debug_console(message)

Output debug messages to the console:

get "/debug" do |req, res|
  debug_console("[Debug] Request received: #{req.path}")
  debug_console("[Debug] Headers: #{req.headers.inspect}")
  
  res.status_code = 200
  res.body = "Check console for debug output"
  res
end

Note: The exact behavior of debug_console depends on the platform:

  • Cloudflare Workers: Outputs to Workers console
  • Fastly Compute: Outputs to Fastly logs
  • Local development: Outputs to stdout

Error Handling

If a route is not found, Uzumibi automatically returns a 404 response:

Status: 404 Not Found
Body: "Not Found"

To handle errors in your route:

post "/api/data" do |req, res|
  begin
    data = JSON.parse(req.body)
    # Process data...
    
    res.status_code = 200
    res.body = "Success"
  rescue JSON::ParserError => e
    res.status_code = 400
    res.body = "Invalid JSON: #{e.message}"
  rescue => e
    res.status_code = 500
    res.body = "Error: #{e.message}"
  end
  res
end

Best Practices

  1. Always return res: Make sure to return the response object from every route handler
  2. Set Content-Type: Always set appropriate Content-Type header
  3. Use appropriate status codes: Return correct HTTP status codes for different scenarios
  4. Validate input: Check and validate request parameters and body
  5. Keep routes simple: Complex logic should be extracted into helper methods or classes
  6. Use debug_console carefully: Excessive logging can impact performance

Limitations

Due to the constrained edge environment and mruby limitations:

  • No file system access
  • Limited Ruby standard library
  • No native C extensions
  • Memory constraints vary by platform
  • No background jobs or async processing within a request

See Supported Platforms for platform-specific limitations and features.

Supported Platforms

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

Sections

Cloudflare Workers

Cloudflare Workers is a serverless platform that runs JavaScript and WebAssembly at the edge.

Features

  • Global Distribution: Runs in 300+ data centers worldwide
  • V8 Engine: Fast JavaScript runtime with WebAssembly support
  • KV Storage: Built-in key-value storage (TBA)
  • Durable Objects: Stateful objects (TBA)
  • Low Cold Start: Minimal startup latency

Project Setup

Generate a new Cloudflare Workers project:

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

Configuration

Edit wrangler.jsonc to configure your Worker:

{
  "name": "my-app",
  "main": "src/index.js",
  "compatibility_date": "2024-01-01"
}

Local Development

# Build WASM module
cd wasm-app
cargo build --target wasm32-unknown-unknown --release
cd ..

# Copy WASM file
cp target/wasm32-unknown-unknown/release/*.wasm public/app.wasm

# Start dev server
npx wrangler dev

Deployment

npx wrangler login
npx wrangler deploy

Limitations

  • CPU Time: 50ms on free plan, 50ms-30s on paid plans
  • Memory: 128MB
  • Request Size: 100MB
  • Response Size: Unlimited

Platform-Specific Features

  • Access to Cloudflare KV (TBA)
  • Access to Cloudflare R2 (TBA)
  • Access to Durable Objects (TBA)

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

FeatureCloudflare WorkersFastly ComputeSpinCloud RunService Worker
Execution ModelV8 IsolatesWASIWASIContainerBrowser
Cold StartVery FastVery FastFastSlowerN/A
Max Execution Time50ms-30s60sVaries60minVaries
Memory Limit128MB128-512MBVaries4GB+Browser
Global DistributionYesYesPlatform-dependentRegionalN/A
Cost ModelPer-requestPer-requestPlatform-dependentPer-request + computeFree
MaturityStableStableStableExperimentalExperimental

Choosing a Platform

Consider these factors when choosing a platform:

  • Global Performance: Cloudflare Workers or Fastly Compute for worldwide low latency
  • Execution Time: Cloud Run if you need longer execution times
  • Open Source: Spin for self-hosted or cloud-agnostic deployments
  • Cost: Compare pricing for your expected traffic patterns
  • Integration: Choose based on existing cloud provider relationships
  • Development: Service Worker for local testing and development

External Service Abstractions

External Service Abstractions provide a unified Ruby API to access platform-specific services such as KV stores, caches, and databases — write once, deploy anywhere.

Sections

What are External Service Abstractions?

External Service Abstractions are unified APIs that allow your Uzumibi application to access platform-specific services (like key-value stores, caches, databases, etc.) through a common interface. This abstraction layer enables you to write code once and deploy to multiple platforms without platform-specific modifications.

Each edge platform provides different services with different APIs:

  • Cloudflare Workers has KV, R2, Durable Objects
  • Fastly Compute has KV Store, Edge Dictionary
  • Spin has Key-Value Store, SQLite
  • Cloud Run can access Google Cloud services

External Service Abstractions provide a unified Ruby API that translates to the appropriate platform-specific implementation at runtime.

Benefits

  • Write Once, Deploy Anywhere: Same code works across platforms
  • Platform Independence: Switch platforms without rewriting service access code
  • Consistent API: Familiar Ruby interface regardless of underlying platform
  • Type Safety: Well-defined interfaces reduce errors

Status

Current Status: TBA (To Be Announced)

The External Service Abstractions layer is currently under development. The following sections describe the planned architecture and APIs.

Available Services

KV (Key-Value Store)

Key-value storage for simple data persistence.

Status: TBA

Planned API

# Store a value
Uzumibi::KV.put("user:123", "John Doe")

# Retrieve a value
name = Uzumibi::KV.get("user:123")  # => "John Doe"

# Delete a value
Uzumibi::KV.delete("user:123")

# Check if key exists
exists = Uzumibi::KV.exists?("user:123")  # => true/false

# List keys with prefix
keys = Uzumibi::KV.list(prefix: "user:")  # => ["user:123", "user:456"]

Platform Mapping

PlatformImplementation
Cloudflare WorkersWorkers KV
Fastly ComputeFastly KV Store
SpinSpin KV Store
Cloud RunTBA

Cache

HTTP caching layer for response caching.

Status: TBA

Planned API

# Store in cache
Uzumibi::Cache.put(
  "cache-key", 
  response_body,
  ttl: 3600  # seconds
)

# Retrieve from cache
cached = Uzumibi::Cache.get("cache-key")

# Delete from cache
Uzumibi::Cache.delete("cache-key")

# Clear all cache
Uzumibi::Cache.clear

Platform Mapping

PlatformImplementation
Cloudflare WorkersCache API
Fastly ComputeEdge Cache
SpinTBA
Cloud RunTBA

Secret

Secure storage for API keys, tokens, and sensitive configuration.

Status: TBA

Planned API

# Access secrets
api_key = Uzumibi::Secret.get("API_KEY")
db_password = Uzumibi::Secret.get("DB_PASSWORD")

Platform Mapping

PlatformImplementation
Cloudflare WorkersEnvironment Variables / Secrets
Fastly ComputeSecret Store
SpinSpin Variables
Cloud RunSecret Manager

ObjectStore

Object storage for files and binary data.

Status: TBA

Planned API

# Upload object
Uzumibi::ObjectStore.put(
  "images/photo.jpg",
  image_data,
  content_type: "image/jpeg"
)

# Download object
image_data = Uzumibi::ObjectStore.get("images/photo.jpg")

# Delete object
Uzumibi::ObjectStore.delete("images/photo.jpg")

# List objects
objects = Uzumibi::ObjectStore.list(prefix: "images/")

Platform Mapping

PlatformImplementation
Cloudflare WorkersR2
Fastly ComputeTBA
SpinTBA
Cloud RunCloud Storage

Queue

Message queue for asynchronous task processing.

Status: TBA

Planned API

# Send message to queue
Uzumibi::Queue.send(
  "notifications",
  { user_id: 123, message: "Hello" }
)

# Consume messages (in queue consumer handler)
Uzumibi::Queue.on_message do |message|
  # Process message
  user_id = message[:user_id]
  # ...
end

Platform Mapping

PlatformImplementation
Cloudflare WorkersQueue (Workers for Platforms)
Fastly ComputeTBA
SpinTBA
Cloud RunCloud Tasks / Pub/Sub

SQL

SQL database access.

Status: TBA

Planned API

# Execute query
results = Uzumibi::SQL.query(
  "SELECT * FROM users WHERE id = ?",
  [123]
)

# Execute update
affected = Uzumibi::SQL.execute(
  "UPDATE users SET name = ? WHERE id = ?",
  ["New Name", 123]
)

# Transaction support
Uzumibi::SQL.transaction do |tx|
  tx.execute("INSERT INTO users (name) VALUES (?)", ["Alice"])
  tx.execute("INSERT INTO logs (action) VALUES (?)", ["user_created"])
end

Platform Mapping

PlatformImplementation
Cloudflare WorkersD1
Fastly ComputeTBA
SpinSQLite
Cloud RunCloud SQL

Fetch

HTTP client for making requests to external APIs.

Status: TBA

Planned API

# GET request
response = Uzumibi::Fetch.get("https://api.example.com/data")
data = JSON.parse(response.body)

# POST request
response = Uzumibi::Fetch.post(
  "https://api.example.com/users",
  body: JSON.generate({ name: "Alice" }),
  headers: {
    "Content-Type" => "application/json",
    "Authorization" => "Bearer #{token}"
  }
)

# Full request
response = Uzumibi::Fetch.request(
  method: "PUT",
  url: "https://api.example.com/resource",
  headers: { "Content-Type" => "application/json" },
  body: JSON.generate({ data: "value" })
)

Platform Mapping

PlatformImplementation
Cloudflare Workersfetch() API
Fastly ComputeBackend requests
SpinOutbound HTTP
Cloud RunHTTP client

Others

Additional services being considered:

  • Analytics: Request analytics and metrics
  • Logging: Structured logging
  • Tracing: Distributed tracing
  • Email: Email sending
  • Websockets: WebSocket connections (platform-dependent)

Platform Support Matrix

ServiceCloudflareFastlySpinCloud Run
KV✅ Workers KV✅ KV Store✅ KV Store❌ TBA
Cache✅ Cache API✅ Edge Cache❌ TBA❌ TBA
Secret✅ Secrets✅ Secret Store✅ Variables✅ Secret Manager
ObjectStore✅ R2❌ TBA❌ TBA✅ Cloud Storage
Queue✅ Queues❌ TBA❌ TBA✅ Pub/Sub
SQL✅ D1❌ TBA✅ SQLite✅ Cloud SQL
Fetch✅ fetch API✅ Backends✅ Outbound HTTP✅ HTTP

Legend:

  • ✅ Planned/Available
  • ❌ Not Available/TBA
  • TBA: To Be Announced

Usage Examples

Example 1: KV-Backed API

class App < Uzumibi::Router
  get "/counter" do |req, res|
    # Get current count
    count = Uzumibi::KV.get("counter")
    count = count ? count.to_i : 0
    
    res.status_code = 200
    res.headers = { "Content-Type" => "application/json" }
    res.body = JSON.generate({ count: count })
    res
  end
  
  post "/counter/increment" do |req, res|
    # Increment counter
    count = Uzumibi::KV.get("counter")
    count = count ? count.to_i + 1 : 1
    Uzumibi::KV.put("counter", count.to_s)
    
    res.status_code = 200
    res.headers = { "Content-Type" => "application/json" }
    res.body = JSON.generate({ count: count })
    res
  end
end

Example 2: Cached API Response

class App < Uzumibi::Router
  get "/weather/:city" do |req, res|
    city = req.params[:city]
    cache_key = "weather:#{city}"
    
    # Try to get from cache
    cached = Uzumibi::Cache.get(cache_key)
    
    if cached
      res.status_code = 200
      res.headers = { 
        "Content-Type" => "application/json",
        "X-Cache" => "HIT"
      }
      res.body = cached
    else
      # Fetch from external API
      weather_res = Uzumibi::Fetch.get(
        "https://api.weather.com/#{city}"
      )
      
      # Cache for 1 hour
      Uzumibi::Cache.put(cache_key, weather_res.body, ttl: 3600)
      
      res.status_code = 200
      res.headers = { 
        "Content-Type" => "application/json",
        "X-Cache" => "MISS"
      }
      res.body = weather_res.body
    end
    
    res
  end
end

Example 3: Database-Backed Application

class App < Uzumibi::Router
  get "/users/:id" do |req, res|
    user_id = req.params[:id].to_i
    
    results = Uzumibi::SQL.query(
      "SELECT * FROM users WHERE id = ?",
      [user_id]
    )
    
    if results.length > 0
      user = results[0]
      res.status_code = 200
      res.headers = { "Content-Type" => "application/json" }
      res.body = JSON.generate(user)
    else
      res.status_code = 404
      res.body = "User not found"
    end
    
    res
  end
  
  post "/users" do |req, res|
    data = JSON.parse(req.body)
    
    Uzumibi::SQL.execute(
      "INSERT INTO users (name, email) VALUES (?, ?)",
      [data["name"], data["email"]]
    )
    
    res.status_code = 201
    res.headers = { "Content-Type" => "application/json" }
    res.body = JSON.generate({ created: true })
    res
  end
end

Development Roadmap

The External Service Abstractions are being developed in phases:

Phase 1: Core Services

  • KV Store
  • Fetch (HTTP Client)
  • Secret Management

Phase 2: Storage

  • Cache
  • Object Store

Phase 3: Advanced Services

  • SQL Database
  • Queue
  • Logging & Analytics

Phase 4: Platform-Specific Features

  • Platform-specific optimizations
  • Advanced features

Contributing

The External Service Abstractions layer is under active development. If you’re interested in contributing or have suggestions for the API design, please:

  1. Check the GitHub repository for current status
  2. Open issues for API suggestions
  3. Submit PRs for implementations

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

uzumibi new

Create a new edge application project from a template.

Synopsis

uzumibi new --template <TEMPLATE> <PROJECT_NAME>

Arguments

  • <PROJECT_NAME>: The name of your project. This will be used as the directory name.

Options

  • -t, --template <TEMPLATE>: The platform template to use. Required.

Available Templates

TemplateDescriptionStatus
cloudflareCloudflare WorkersStable
fastlyFastly Compute@EdgeStable
spinSpin (Fermyon)Stable
cloudrunGoogle Cloud RunExperimental

Examples

Create a Cloudflare Workers project:

uzumibi new --template cloudflare my-worker

Create a Fastly Compute project:

uzumibi new --template fastly my-compute-app

Create a Spin project:

uzumibi new --template spin my-spin-app

Create a Cloud Run project:

uzumibi new --template cloudrun my-cloudrun-app

What Gets Created

The uzumibi new command generates a complete project structure including:

  • Cargo.toml: Rust workspace configuration
  • build.rs: Build script that compiles Ruby to mruby bytecode
  • lib/app.rb: Your Ruby application code (main entry point)
  • src/: Platform-specific Rust/JavaScript code
  • Platform-specific configuration files:
    • wrangler.jsonc (Cloudflare)
    • fastly.toml (Fastly)
    • spin.toml (Spin)
    • Dockerfile (Cloud Run)

Example project structure for Cloudflare Workers:

my-worker/
├── Cargo.toml
├── package.json
├── pnpm-lock.yaml
├── wrangler.jsonc
├── lib/
│   └── app.rb
├── src/
│   └── index.js
└── wasm-app/
    ├── Cargo.toml
    ├── build.rs
    └── src/
        └── lib.rs

uzumibi --help

Display help information:

uzumibi --help

Output:

Uzumibi CLI - Create a new edge application project powered by Ruby

Usage: uzumibi <COMMAND>

Commands:
  new   Create a new edge application project
  help  Print this message or the help of the given subcommand(s)

Options:
  -h, --help     Print help
  -V, --version  Print version

uzumibi --version

Display the CLI version:

uzumibi --version

Project Templates

Cloudflare Workers Template

Files included:

  • wrangler.jsonc: Wrangler configuration
  • package.json: Node.js dependencies (for Wrangler)
  • src/index.js: JavaScript entry point
  • wasm-app/: Rust WASM module source

Build with:

cd wasm-app
cargo build --target wasm32-unknown-unknown --release

Run locally:

npx wrangler dev

Deploy:

npx wrangler deploy

Fastly Compute Template

Files included:

  • fastly.toml: Fastly service configuration
  • Cargo.toml: Rust project configuration
  • src/main.rs: Application entry point
  • src/lib.rs: WASM module

Build with:

cargo build --target wasm32-wasi --release

Run locally:

fastly compute serve

Deploy:

fastly compute deploy

Spin Template

Files included:

  • spin.toml: Spin application manifesthown
  • Cargo.toml: Rust project configuration
  • src/lib.rs: Application entry point

Build with:

spin build

Run locally:

spin up

Deploy:

spin deploy

Cloud Run Template

Files included:

  • Dockerfile: Container image definition
  • Cargo.toml: Rust project configuration
  • src/main.rs: HTTP server entry point
  • src/uzumibi.rs: Uzumibi integration

Build with:

cargo build --release

Run locally:

cargo run

Deploy:

gcloud builds submit --tag gcr.io/PROJECT_ID/app
gcloud run deploy --image gcr.io/PROJECT_ID/app

Common Workflows

Creating and Deploying a New App

# 1. Create project
uzumibi new --template cloudflare my-app

# 2. Navigate to project
cd my-app

# 3. Edit your Ruby code
vim lib/app.rb

# 4. Build WASM module
cd wasm-app
cargo build --target wasm32-unknown-unknown --release
cd ..

# 5. Test locally
npx wrangler dev

# 6. Deploy
npx wrangler deploy

Updating Ruby Code

After modifying lib/app.rb, rebuild the WASM module:

cd wasm-app
cargo build --target wasm32-unknown-unknown --release
cd ..

The build.rs script automatically compiles your Ruby code to mruby bytecode during the Cargo build process.

Adding Dependencies

To add Rust crates to your project:

cd wasm-app  # or project root for non-Cloudflare projects
cargo add <crate-name>

Note: mruby/Ruby dependencies are limited to what’s available in the mruby/edge runtime.

Troubleshooting

“uzumibi: command not found”

Make sure ~/.cargo/bin is in your PATH:

export PATH="$HOME/.cargo/bin:$PATH"

Add this to your shell profile (~/.bashrc, ~/.zshrc, etc.) to make it permanent.

“Invalid template”

Check that you’re using a valid template name:

  • cloudflare
  • fastly
  • spin
  • cloudrun

Template names are case-sensitive and must be lowercase.

“Directory already exists”

The CLI won’t overwrite existing directories. Either:

  1. Choose a different project name
  2. Remove the existing directory
  3. Use a different location

Environment Variables

CARGO_TARGET_DIR

Override the default Cargo target directory:

export CARGO_TARGET_DIR=/path/to/target
uzumibi new --template cloudflare my-app

Future Commands

Planned additions to the CLI:

  • uzumibi build: Build the project for the current platform
  • uzumibi dev: Start local development server
  • uzumibi deploy: Deploy to the configured platform
  • uzumibi init: Initialize Uzumibi in an existing project
  • uzumibi add-service: Add external service integration

These commands are not yet implemented but are planned for future releases.

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.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.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.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.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.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.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