spikard

Crates.iospikard
lib.rsspikard
version0.9.2
created_at2025-11-23 13:06:57.618138+00
updated_at2026-01-22 13:16:50.477275+00
descriptionHigh-performance HTTP framework built on Axum and Tower-HTTP with type-safe routing, validation, WebSocket/SSE support, and lifecycle hooks
homepagehttps://github.com/Goldziher/spikard
repositoryhttps://github.com/Goldziher/spikard
max_upload_size
id1946522
size162,521
Na'aman Hirschfeld (Goldziher)

documentation

https://docs.rs/spikard

README

Spikard

High-performance HTTP framework built on Axum and Tower-HTTP with type-safe routing, validation, WebSocket/SSE support, and lifecycle hooks.

Status & Badges

Crates.io Downloads Documentation License Discord codecov

Multi-Language Bindings

PyPI npm RubyGems Packagist

Features

  • Type-safe routing with path parameter extraction and compile-time validation
  • JSON Schema validation via schemars with automatic OpenAPI generation
  • WebSocket and SSE (Server-Sent Events) support
  • Lifecycle hooks with zero-cost abstraction (onRequest, preValidation, preHandler, onResponse, onError)
  • Tower middleware stack (compression, rate limiting, auth, CORS, request IDs, timeouts)
  • OpenAPI 3.1 and AsyncAPI generation with Swagger UI and ReDoc
  • Testing utilities with in-memory test server
  • File upload handling with multipart form support
  • Streaming responses with native async/await
  • Multi-language bindings (Python, Node.js, Ruby, PHP, WebAssembly)

Installation

Rust

[dependencies]
spikard = "0.9.2"
serde = { version = "1.0", features = ["derive"] }
schemars = "0.8"  # For JSON Schema generation
tokio = { version = "1", features = ["full"] }

Quick Start

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(())
}

Route Registration

RouteBuilder API

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>();

With Raw JSON Schema

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);

Request Context

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))
}

Configuration

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);

Lifecycle Hooks

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()
};

WebSockets

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);

Server-Sent Events

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),
});

File Uploads

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 */)
}

Testing

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

Integration with Axum

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);

Running

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let app = App::new();
    app.run().await?;
    Ok(())
}

Type Safety

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 { /* ... */ }

Features

  • Type-safe routing with path parameter extraction
  • JSON Schema validation via schemars
  • WebSocket and SSE support
  • Lifecycle hooks with zero-cost abstraction
  • Tower middleware (compression, rate limiting, auth, CORS, etc.)
  • OpenAPI 3.1 generation
  • Testing utilities with in-memory server
  • File upload handling
  • Streaming responses

Performance

Built on:

  • Axum for routing and handlers
  • Tower-HTTP for middleware
  • Tokio for async runtime
  • jsonschema for validation
  • Zero-copy where possible

Language Bindings

Spikard is available for multiple languages:

Python

pip install spikard

See spikard-py for details.

Node.js / TypeScript

npm install spikard

See spikard-node for details.

Ruby

gem install spikard

See spikard-rb for details.

PHP

composer require spikard/spikard

See spikard-php for details.

WebAssembly

npm install spikard-wasm

See spikard-wasm for details.

Documentation

Examples

See /examples/rust/ for more Rust examples.

Performance

Built on industry-proven foundations:

  • Axum for blazing-fast routing (600k+ req/s)
  • Tower-HTTP for zero-overhead middleware
  • Tokio for high-performance async runtime
  • jsonschema for efficient validation

Benchmark Results

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.

Related Projects

License

MIT

Commit count: 1542

cargo fmt