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
- uzumibi-cli: Command-line tool for generating project scaffolds
- uzumibi-gem: Core framework providing the Router class and request/response handling
- uzumibi-art-router: Lightweight router library for path matching and parameter extraction
- mruby/edge: Ruby runtime optimized for edge computing
- Platform Adapters: Platform-specific code for each edge provider
Request Flow
- HTTP request arrives at the edge platform
- Platform routes to WASM module
- Request data is serialized and passed to mruby/edge
- Your Router class processes the request
- Response is generated and serialized
- 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
- Installing via cargo
- Creating a Cloudflare Workers Project
- Editing Ruby Files
- Running Locally
- Deploying
- Troubleshooting
- Next Steps
Prerequisites
Before you begin, make sure you have:
- Rust toolchain (1.70 or later)
wasm32-unknown-unknowntarget installed- Platform-specific tools (e.g.,
wranglerfor 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 Workersfastly: Fastly Compute@Edgespin: 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
- Learn about the Ruby API for routing and request/response handling
- Explore supported platforms and platform-specific features
- Check out external service abstractions for KV stores, caching, etc.
Troubleshooting
Build Errors
If you encounter build errors, make sure:
-
The
wasm32-unknown-unknowntarget is installed:rustup target add wasm32-unknown-unknown -
For Fastly, install the
wasm32-wasitarget: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:
- Use release builds (already configured)
- Strip debug symbols (already configured)
- Minimize Ruby code and dependencies
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
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- OK201- Created204- No Content301- Moved Permanently302- Found (Redirect)400- Bad Request401- Unauthorized403- Forbidden404- Not Found500- 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
- Always return
res: Make sure to return the response object from every route handler - Set Content-Type: Always set appropriate
Content-Typeheader - Use appropriate status codes: Return correct HTTP status codes for different scenarios
- Validate input: Check and validate request parameters and body
- Keep routes simple: Complex logic should be extracted into helper methods or classes
- 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
- Fastly Compute
- Spin (Fermyon Cloud)
- Cloud Run
- Service Worker/Web Worker (Experimental)
- Platform Comparison
- Choosing a Platform
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
- 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
| Feature | Cloudflare Workers | Fastly Compute | Spin | Cloud Run | Service Worker |
|---|---|---|---|---|---|
| Execution Model | V8 Isolates | WASI | WASI | Container | Browser |
| Cold Start | Very Fast | Very Fast | Fast | Slower | N/A |
| Max Execution Time | 50ms-30s | 60s | Varies | 60min | Varies |
| Memory Limit | 128MB | 128-512MB | Varies | 4GB+ | Browser |
| Global Distribution | Yes | Yes | Platform-dependent | Regional | N/A |
| Cost Model | Per-request | Per-request | Platform-dependent | Per-request + compute | Free |
| Maturity | Stable | Stable | Stable | Experimental | Experimental |
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?
- Available Services
- Platform Support Matrix
- Usage Examples
- Development Roadmap
- Contributing
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
| Platform | Implementation |
|---|---|
| Cloudflare Workers | Workers KV |
| Fastly Compute | Fastly KV Store |
| Spin | Spin KV Store |
| Cloud Run | TBA |
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
| Platform | Implementation |
|---|---|
| Cloudflare Workers | Cache API |
| Fastly Compute | Edge Cache |
| Spin | TBA |
| Cloud Run | TBA |
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
| Platform | Implementation |
|---|---|
| Cloudflare Workers | Environment Variables / Secrets |
| Fastly Compute | Secret Store |
| Spin | Spin Variables |
| Cloud Run | Secret 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
| Platform | Implementation |
|---|---|
| Cloudflare Workers | R2 |
| Fastly Compute | TBA |
| Spin | TBA |
| Cloud Run | Cloud 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
| Platform | Implementation |
|---|---|
| Cloudflare Workers | Queue (Workers for Platforms) |
| Fastly Compute | TBA |
| Spin | TBA |
| Cloud Run | Cloud 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
| Platform | Implementation |
|---|---|
| Cloudflare Workers | D1 |
| Fastly Compute | TBA |
| Spin | SQLite |
| Cloud Run | Cloud 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
| Platform | Implementation |
|---|---|
| Cloudflare Workers | fetch() API |
| Fastly Compute | Backend requests |
| Spin | Outbound HTTP |
| Cloud Run | HTTP 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
| Service | Cloudflare | Fastly | Spin | Cloud 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:
- Check the GitHub repository for current status
- Open issues for API suggestions
- Submit PRs for implementations
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
- Future Commands
- Updating the CLI
- Getting Help
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
| Template | Description | Status |
|---|---|---|
cloudflare | Cloudflare Workers | Stable |
fastly | Fastly Compute@Edge | Stable |
spin | Spin (Fermyon) | Stable |
cloudrun | Google Cloud Run | Experimental |
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 configurationpackage.json: Node.js dependencies (for Wrangler)src/index.js: JavaScript entry pointwasm-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 configurationCargo.toml: Rust project configurationsrc/main.rs: Application entry pointsrc/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 manifesthownCargo.toml: Rust project configurationsrc/lib.rs: Application entry point
Build with:
spin build
Run locally:
spin up
Deploy:
spin deploy
Cloud Run Template
Files included:
Dockerfile: Container image definitionCargo.toml: Rust project configurationsrc/main.rs: HTTP server entry pointsrc/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:
cloudflarefastlyspincloudrun
Template names are case-sensitive and must be lowercase.
“Directory already exists”
The CLI won’t overwrite existing directories. Either:
- Choose a different project name
- Remove the existing directory
- 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 platformuzumibi dev: Start local development serveruzumibi deploy: Deploy to the configured platformuzumibi init: Initialize Uzumibi in an existing projectuzumibi 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 --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.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