Spikard Elixir
Elixir bindings for Spikard: a Rust-centric web framework with type-safe code generation from OpenAPI, GraphQL, AsyncAPI, and OpenRPC specifications. Leverage Phoenix-style routing with zero-copy Rustler NIF performance.
Key Features
- Phoenix-style routing: Compile-time
get,post,put,patch,deletemacros with scopes - Zero-copy NIFs: Rustler bindings eliminate serialization overhead
- Full typespecs:
@specannotations on all public functions for Dialyzer type checking - Code generation: Generate type-safe handlers from OpenAPI, GraphQL, AsyncAPI, and OpenRPC specs
- 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, background tasks
Installation
Add spikard to your list of dependencies in mix.exs:
def deps do
[
{:spikard, "~> 0.13.0"}
]
endRequirements: Elixir 1.18+ with OTP 27+. Normal installs use precompiled NIFs; Rust is only required when forcing a source build (for example with SPIKARD_BUILD=1).
Quick Start
defmodule MyApp.Router do
use Spikard.Router
get "/", &hello/1
get "/users/:id", &show_user/1
post "/users", &create_user/1
defp hello(_request) do
Spikard.Response.json(%{message: "Hello, World!"})
end
defp show_user(request) do
user_id = Spikard.Request.get_path_param(request, "id")
Spikard.Response.json(%{id: user_id, name: "Alice"})
end
defp create_user(request) do
body = Spikard.Request.get_body(request)
Spikard.Response.json(%{id: 1, name: body["name"]}, status: 201)
end
end
{:ok, server} = Spikard.start(MyApp.Router, port: 4000)
The Spikard.Request struct provides access to:
get_path_param(request, key)- Path parametersget_query_param(request, key, default)- Query parametersget_header(request, name)- Request headers (case-insensitive)get_cookie(request, name)- Request cookiesget_body(request)- Parsed request bodyfiles(request)- Uploaded files
Responses
Return maps, or use the fluent Response builder:
# Simple JSON (auto-serialized)
Spikard.Response.json(%{message: "Hello"})
# Custom status and headers
Spikard.Response.new()
|> Spikard.Response.with_status(201)
|> Spikard.Response.with_json(%{id: 1})
|> Spikard.Response.with_header("x-request-id", "abc123")
|> Spikard.Response.with_cookie("session", "xyz", http_only: true, secure: true)
# Streaming response
Spikard.Response.stream(Stream.interval(1000) |> Stream.map(&"tick #{&1}"))Configuration
Configure the server with middleware options:
{:ok, server} = Spikard.start(MyApp.Router,
port: 4000,
host: "0.0.0.0",
config: [
compression: %{gzip: true, brotli: true},
rate_limit: %{per_second: 100},
cors: %{
allowed_origins: ["https://example.com"],
allowed_methods: ["GET", "POST", "OPTIONS"],
allowed_headers: ["content-type", "authorization"],
max_age: 3600
},
jwt: %{secret: "your-secret", algorithm: "HS256"},
static_files: %{directory: "./priv/static"}
]
)Dependency Injection
Inject dependencies into handlers:
db = %{host: "localhost", port: 5432}
{:ok, server} = Spikard.start(MyApp.Router,
port: 4000,
dependencies: [
Spikard.DI.value("database", db),
Spikard.DI.factory("request_id", fn -> System.unique_integer() end, singleton: false)
]
)
# In handler:
def show(request) do
db = Spikard.Request.get_dependency(request, "database")
Spikard.Response.json(%{host: db.host})
endLifecycle Hooks
Execute logic at key points in the request lifecycle:
auth_hook = fn ctx ->
case Map.get(ctx.headers, "authorization") do
"Bearer " <> _token -> {:continue, ctx}
_ -> {:short_circuit, %{status: 401, body: %{error: "Unauthorized"}}}
end
end
{:ok, server} = Spikard.start(MyApp.Router,
port: 4000,
lifecycle: [
on_request: [fn ctx -> IO.inspect(ctx.path); {:continue, ctx} end],
pre_handler: [auth_hook],
on_response: [fn res -> {:continue, res} end]
]
)
Hooks can return {:continue, context} to proceed or {:short_circuit, response} to respond immediately.
Real-Time Communication
WebSockets:
defmodule ChatHandler do
use Spikard.WebSocket
def handle_connect(_ws, _opts), do: {:ok, %{}}
def handle_message(msg, state), do: {:reply, msg, state}
def handle_disconnect(_ws, _state), do: :ok
end
# In router:
websocket "/chat", ChatHandlerServer-Sent Events:
defmodule TickProducer do
use Spikard.Sse.Producer
def init(_opts), do: {:ok, 0}
def next_event(count) when count < 100 do
{:ok, %Spikard.Sse.Event{data: count, event: "tick"}, count + 1}
end
def next_event(_), do: :done
end
# In router:
sse "/events", TickProducerBackground Tasks
Offload work after sending response:
def process(request) do
Spikard.Background.run(fn -> send_notification_email() end)
Spikard.Response.json(%{status: "processing"})
endFile Uploads
Handle multipart file uploads:
def upload(request) do
case Spikard.Request.files(request) do
[file | _] ->
Spikard.Response.json(%{
filename: file.filename,
size: file.size,
is_image: Spikard.UploadFile.image?(file)
})
[] ->
Spikard.Response.json(%{error: "No file uploaded"}, status: 400)
end
endTesting
Use the TestClient for integration tests without network overhead:
defmodule MyAppTest do
use ExUnit.Case
test "GET / returns hello" do
{:ok, client} = Spikard.TestClient.new(routes: MyApp.Router.routes())
{:ok, response} = Spikard.TestClient.get(client, "/")
assert response.status_code == 200
assert Spikard.TestClient.Response.json(response) == %{"message" => "Hello, World!"}
end
endExamples
Examples & Code Generation:
- Runnable Examples - Elixir, Python, TypeScript, Ruby, PHP, and Rust
- Code Generation Guide - Generate from OpenAPI, GraphQL, AsyncAPI, OpenRPC, Protobuf
Documentation
Full documentation at spikard.dev. See also CONTRIBUTING.md.
Other Languages
License
MIT - See LICENSE for details