Spikard Ruby

Documentation Gem Version Gem Downloads Ruby Version codecov License

Ruby bindings for Spikard: a Rust-centric web framework with type-safe code generation from OpenAPI, GraphQL, AsyncAPI, and OpenRPC specifications. Leverage Sinatra-style routing with zero-copy FFI performance.

Key Features

  • Type-safe with RBS: Full RBS type definitions for Steep type checking
  • Zero-copy FFI: Magnus/rb-sys bindings eliminate serialization overhead
  • Sinatra-style routing: Familiar get, post, put, patch, delete DSL
  • Code generation: Generate type-safe handlers from OpenAPI, GraphQL, AsyncAPI, and OpenRPC specs
  • Full async support: Non-blocking handlers with complete async/await integration
  • Tower-HTTP middleware: Compression, rate limiting, authentication, CORS, request IDs
  • Real-time: WebSockets and Server-Sent Events (SSE)
  • Production-ready: Dependency injection, validation schemas, lifecycle hooks

Installation

Via RubyGems (recommended):

gem install spikard

From source:

bundle install
bundle exec rake ext:build

Requirements: Ruby 3.2+, Bundler, and Rust toolchain (for building from source). On Windows, use RubyInstaller with MSYS2 DevKit and the GNU Rust toolchain.

Quick Start

require "spikard"

app = Spikard::App.new

app.get "/hello" do |request|
  { message: "Hello, World!" }
end

app.get "/users/:id" do |request|
  user_id = request[:path_params]["id"]
  { id: user_id, name: "Alice" }
end

app.post "/users" do |request|
  { id: 1, name: request[:body]["name"] }
end

app.run(port: 8000)

The request hash provides access to:

  • request[:method] - HTTP method
  • request[:path] - URL path
  • request[:path_params] - Path parameters
  • request[:query] - Query parameters
  • request[:headers] - Request headers
  • request[:cookies] - Request cookies
  • request[:body] - Parsed request body

Validation

Pass a request_schema to validate incoming JSON:

require "dry-schema"

UserSchema = Dry::Schema.JSON do
  required(:name).filled(:str?)
  required(:email).filled(:str?)
end

app.post "/users", request_schema: UserSchema do |request|
  { id: 1, name: request[:body]["name"] }
end

Also supports raw JSON Schema objects and dry-struct schemas.

Dependency Injection

Inject dependencies as keyword parameters:

app.provide("config", { "db_url" => "postgresql://localhost" })
app.provide("db", depends_on: ["config"], singleton: true) { |config:| Pool.new(config) }

app.get "/data" do |request, config:, db:|
  { url: config["db_url"] }
end

Responses

Return a Hash, String, or Response object:

# Simple hash (auto-serialized to JSON)
app.get "/hello" do
  { message: "Hello, World!" }
end

# Custom status and headers
app.post "/users" do |request|
  Spikard::Response.new(
    content: { id: 1 },
    status_code: 201,
    headers: { "X-Custom" => "value" }
  )
end

# Streaming response
app.get "/stream" do
  stream = Enumerator.new { |y| y << "data" }
  Spikard::StreamingResponse.new(stream)
end

# File uploads
app.post "/upload", file_params: true do |request|
  file = request[:body]["file"]
  { filename: file.filename, size: file.size }
end

Configuration

Configure the server with middleware options:

config = Spikard::ServerConfig.new(
  host: "0.0.0.0",
  port: 8080,
  workers: 4,
  compression: Spikard::CompressionConfig.new(gzip: true, brotli: true),
  rate_limit: Spikard::RateLimitConfig.new(per_second: 100),
  jwt: Spikard::JwtConfig.new(secret: "key", algorithm: "HS256"),
  static_files: Spikard::StaticFilesConfig.new(directory: "./public"),
  max_body_size: 10 * 1024 * 1024,
  request_timeout: 30
)

app.run(config: config)

See Configuration Reference for full options.

Lifecycle Hooks

Execute logic at key points in the request lifecycle:

app.on_request { |req| puts "#{req[:method]} #{req[:path]}" }
app.pre_validation { |req| req }
app.pre_handler { |req| req }
app.on_response { |res| res }
app.on_error { |res| res }

Return a Request/Response object to continue, or a Response to short-circuit.

Real-Time Communication

WebSockets:

class ChatHandler < Spikard::WebSocketHandler
  def handle_message(message) = { echo: message }
end

app.websocket("/chat") { ChatHandler.new }

Server-Sent Events:

class Events < Spikard::SseEventProducer
  def next_event = Spikard::SseEvent.new(data: { msg: "Hello" })
end

app.sse("/events") { Events.new }

Background Tasks

Offload work after sending response:

app.post "/process" do
  Spikard::Background.run { perform_long_task }
  { status: "processing" }
end

Testing

Use the TestClient for integration tests:

client = Spikard::TestClient.new(app)

# HTTP requests
response = client.get("/hello", query: { name: "Alice" })
puts response.status_code  # 200
puts response.json         # { "message" => "Hello, World!" }

# POST, WebSocket, SSE all supported
response = client.post("/users", json: { name: "Bob" })
ws = client.websocket("/chat")
sse = client.sse("/events")

client.close

Type Safety with RBS & Steep

Full RBS type definitions are included in sig/spikard.rbs. Use Steep for type checking:

bundle exec steep check

Performance

Built with zero-overhead FFI via Magnus and rb-sys. Benchmark results: ~8,000 RPS, ~6.5ms latency at 50 concurrency. See benchmarks for full results.

Learn More

Examples & Code Generation:

Documentation:

Other Languages:

License

MIT - See LICENSE for details