| Crates.io | spikard |
| lib.rs | spikard |
| version | 0.9.2 |
| created_at | 2025-11-23 13:06:57.618138+00 |
| updated_at | 2026-01-22 13:16:50.477275+00 |
| description | High-performance HTTP framework built on Axum and Tower-HTTP with type-safe routing, validation, WebSocket/SSE support, and lifecycle hooks |
| homepage | https://github.com/Goldziher/spikard |
| repository | https://github.com/Goldziher/spikard |
| max_upload_size | |
| id | 1946522 |
| size | 162,521 |
High-performance HTTP framework built on Axum and Tower-HTTP with type-safe routing, validation, WebSocket/SSE support, and lifecycle hooks.
[dependencies]
spikard = "0.9.2"
serde = { version = "1.0", features = ["derive"] }
schemars = "0.8" # For JSON Schema generation
tokio = { version = "1", features = ["full"] }
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use spikard::prelude::*;
#[derive(Deserialize, Serialize, JsonSchema)]
struct User {
id: u64,
name: String,
email: String,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut app = App::new();
app.route(get("/users/:id"), |ctx: Context| async move {
let id: u64 = ctx.path_param("id").unwrap_or("0").parse().unwrap_or(0);
Ok(Json(User {
id,
name: "Alice".to_string(),
email: "alice@example.com".to_string(),
}))
})?;
app.route(
post("/users")
.request_body::<User>()
.response_body::<User>(),
|ctx: Context| async move {
let user: User = ctx.json()?;
Ok(Json(user))
},
)?;
app.run().await?;
Ok(())
}
use spikard::{get, post, put, patch, delete};
use schemars::JsonSchema;
#[derive(JsonSchema)]
struct UserParams {
id: u64,
}
#[derive(JsonSchema)]
struct CreateUser {
name: String,
email: String,
}
let route = get("/users/:id")
.handler_name("get_user_by_id")
.params::<UserParams>();
let create_route = post("/users")
.request_body::<CreateUser>()
.response_body::<User>();
use serde_json::json;
let schema = json!({
"type": "object",
"properties": {
"name": { "type": "string" },
"email": { "type": "string", "format": "email" }
},
"required": ["name", "email"]
});
let route = post("/users")
.request_schema_json(schema);
Access request data in handlers:
use spikard::prelude::*;
async fn handler(ctx: Context) -> HandlerResult {
// Parse JSON body
let body: MyStruct = ctx.json()?;
// Query parameters
let query: QueryParams = ctx.query()?;
// Path parameters
let id = ctx.path_param("id").unwrap();
let path_data: PathParams = ctx.path()?;
// Headers
let auth = ctx.header("authorization");
// Cookies
let session = ctx.cookie("session_id");
// Request metadata
let method = ctx.method();
let path = ctx.path_str();
Ok(Json(body))
}
use spikard::{
App, ServerConfig, CompressionConfig, RateLimitConfig,
JwtConfig, StaticFilesConfig, OpenApiConfig
};
let config = ServerConfig {
host: "0.0.0.0".to_string(),
port: 8080,
workers: 4,
enable_request_id: true,
max_body_size: Some(10 * 1024 * 1024),
request_timeout: Some(30),
compression: Some(CompressionConfig {
gzip: true,
brotli: true,
min_size: 1024,
quality: 6,
}),
rate_limit: Some(RateLimitConfig {
per_second: 100,
burst: 200,
ip_based: true,
}),
jwt_auth: Some(JwtConfig {
secret: "your-secret".to_string(),
algorithm: "HS256".to_string(),
audience: None,
issuer: None,
leeway: 0,
}),
static_files: vec![
StaticFilesConfig {
directory: "./public".to_string(),
route_prefix: "/static".to_string(),
index_file: true,
cache_control: Some("public, max-age=3600".to_string()),
}
],
openapi: Some(OpenApiConfig {
enabled: true,
title: "My API".to_string(),
version: "1.0.0".to_string(),
description: Some("API documentation".to_string()),
swagger_ui_path: "/docs".to_string(),
redoc_path: "/redoc".to_string(),
..Default::default()
}),
..Default::default()
};
let app = App::new().config(config);
use spikard::{LifecycleHooks, request_hook, response_hook, HookResult};
use std::sync::Arc;
let hooks = LifecycleHooks::builder()
.on_request(request_hook("logger", |req| async move {
println!("Request: {} {}", req.method(), req.uri());
Ok(HookResult::Continue(req))
}))
.pre_validation(request_hook("auth", |req| async move {
// Authentication check
Ok(HookResult::Continue(req))
}))
.pre_handler(request_hook("rate_limit", |req| async move {
// Rate limiting
Ok(HookResult::Continue(req))
}))
.on_response(response_hook("headers", |mut resp| async move {
resp.headers_mut().insert(
"X-Frame-Options",
axum::http::HeaderValue::from_static("DENY")
);
Ok(HookResult::Continue(resp))
}))
.on_error(response_hook("error_log", |resp| async move {
eprintln!("Error: {}", resp.status());
Ok(HookResult::Continue(resp))
}))
.build();
let config = ServerConfig {
lifecycle_hooks: Some(Arc::new(hooks)),
..Default::default()
};
use spikard::WebSocketHandler;
use serde_json::Value;
struct EchoHandler;
impl WebSocketHandler for EchoHandler {
fn handle_message(&self, message: Value) -> impl std::future::Future<Output = Option<Value>> + Send {
async move { Some(message) } // Echo back
}
fn on_connect(&self) -> impl std::future::Future<Output = ()> + Send {
async {
println!("Client connected");
}
}
fn on_disconnect(&self) -> impl std::future::Future<Output = ()> + Send {
async {
println!("Client disconnected");
}
}
}
app.websocket("/ws", EchoHandler);
use spikard::{SseEventProducer, SseEvent};
use serde_json::json;
struct TickerProducer {
count: std::sync::atomic::AtomicU64,
}
impl SseEventProducer for TickerProducer {
async fn next_event(&self) -> Option<SseEvent> {
let n = self.count.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
if n < 10 {
Some(SseEvent::new(json!({"tick": n})))
} else {
None
}
}
}
app.sse("/events", TickerProducer {
count: std::sync::atomic::AtomicU64::new(0),
});
use spikard::UploadFile;
use serde::Deserialize;
#[derive(Deserialize)]
struct UploadRequest {
file: UploadFile,
description: String,
}
async fn upload_handler(ctx: Context) -> HandlerResult {
let upload: UploadRequest = ctx.json()?;
let content = upload.file.as_bytes();
let filename = &upload.file.filename;
// Process upload...
Ok(/* response */)
}
use spikard::testing::TestServer;
use axum::http::Request;
#[tokio::test]
async fn test_api() {
let mut app = App::new();
// ... configure routes
let server = TestServer::from_app(app).unwrap();
let request = Request::builder()
.uri("http://localhost/users")
.method("GET")
.body(axum::body::Body::empty())
.unwrap();
let response = server.call(request).await.unwrap();
assert_eq!(response.status, 200);
let json = response.json().unwrap();
// assertions...
}
Merge custom Axum routers:
use axum::{Router, routing::get};
async fn health() -> &'static str {
"OK"
}
let custom_router = Router::new()
.route("/health", get(health));
let app = App::new()
.merge_axum_router(custom_router);
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let app = App::new();
app.run().await?;
Ok(())
}
All handlers use Context and return HandlerResult:
pub type HandlerResult = Result<Response<Body>, (StatusCode, String)>;
pub trait IntoHandler {
fn into_handler(self) -> Arc<dyn Handler>;
}
Handlers can be async functions or closures:
async fn handler(ctx: Context) -> HandlerResult { /* ... */ }
|ctx: Context| async move { /* ... */ }
Built on:
Spikard is available for multiple languages:
pip install spikard
See spikard-py for details.
npm install spikard
See spikard-node for details.
gem install spikard
See spikard-rb for details.
composer require spikard/spikard
See spikard-php for details.
npm install spikard-wasm
See spikard-wasm for details.
See /examples/rust/ for more Rust examples.
Built on industry-proven foundations:
Latest comparative run (2025-12-20, commit 25e4fdf, Linux x86_64, AMD EPYC 7763 2c/4t, 50 concurrency, 10s, oha). Full artifacts: snapshots/benchmarks/20397054933.
| Binding | Avg RPS (all workloads) | Avg latency (ms) |
|---|---|---|
| spikard-rust | 55,755 | 1.00 |
| spikard-node | 24,283 | 2.22 |
| spikard-php | 20,176 | 2.66 |
| spikard-python | 11,902 | 4.41 |
| spikard-wasm | 10,658 | 5.70 |
| spikard-ruby | 8,271 | 6.50 |
Spikard Rust is the fastest binding, delivering native performance as the reference implementation. All workloads include JSON, query/path params, forms, and multipart requests.
MIT