Skip to main content

High-performance Python web framework with a Rust core. Build REST APIs, WebSockets, and SSE services with FastAPI/Litestar-style decorators backed by Axum and Tower-HTTP. 2.8x faster than FastAPI.

Project description

Spikard Python

High-performance Python web framework with a Rust core. Build REST APIs, WebSockets, and SSE services with FastAPI/Litestar-style decorators backed by Axum and Tower-HTTP.

Badges

Documentation PyPI Downloads Python codecov License Crates.io npm RubyGems Packagist Discord

Installation

Via pip:

pip install spikard

Pre-built wheels are available for macOS, Linux, and Windows. If a wheel isn't available for your platform, pip will build from source (requires Rust toolchain).

From source (development):

cd packages/python
uv sync
# or
pip install -e .

Requirements:

  • Python 3.10+
  • Rust toolchain (only required for building from source)

Quick Start

from spikard import Spikard
from msgspec import Struct

class User(Struct):
    id: int
    name: str
    email: str

app = Spikard()

@app.get("/users/{user_id}")
async def get_user(user_id: int) -> User:
    return User(id=user_id, name="Alice", email="alice@example.com")

@app.post("/users")
async def create_user(user: User) -> User:
    # Automatic validation via msgspec
    return user

if __name__ == "__main__":
    app.run(port=8000)

Core Features

Route Registration

Spikard supports both FastAPI-style (instance decorators) and Litestar-style (standalone decorators) patterns.

FastAPI-style (instance decorators):

from spikard import Spikard

app = Spikard()

@app.get("/users")
async def list_users():
    return {"users": []}

@app.post("/users")
async def create_user(user: User):
    return user

Litestar-style (standalone decorators):

from spikard import Spikard, get, post, put, patch, delete

app = Spikard()

@get("/users")
async def list_users():
    return {"users": []}

@post("/users")
async def create_user(user: User):
    return user

@put("/users/{user_id}")
async def update_user(user_id: int, user: User):
    return user

@patch("/users/{user_id}")
async def patch_user(user_id: int, updates: dict):
    return updates

@delete("/users/{user_id}")
async def delete_user(user_id: int):
    return {"deleted": True}

All HTTP methods supported:

  • @app.get() / @get() - GET requests
  • @app.post() / @post() - POST requests
  • @app.put() / @put() - PUT requests
  • @app.patch() / @patch() - PATCH requests
  • @app.delete() / @delete() - DELETE requests
  • @app.head() / @head() - HEAD requests
  • @app.options() / @options() - OPTIONS requests
  • @app.trace() / @trace() - TRACE requests

Path Parameters

@app.get("/users/{user_id}")
async def get_user(user_id: int):
    return {"id": user_id}

@app.get("/posts/{post_id}/comments/{comment_id}")
async def get_comment(post_id: int, comment_id: int):
    return {"post_id": post_id, "comment_id": comment_id}

Query Parameters

from spikard import Query

@app.get("/search")
async def search(
    q: str,
    limit: int = Query(default=10),
    offset: int = Query(default=0)
):
    return {"query": q, "limit": limit, "offset": offset}

Request Body Validation

Spikard supports multiple validation libraries. msgspec is the default and recommended for best performance.

With msgspec.Struct (recommended - fastest):

from msgspec import Struct

class CreatePost(Struct):
    title: str
    content: str
    tags: list[str] = []

@app.post("/posts")
async def create_post(post: CreatePost):
    return {"title": post.title, "tag_count": len(post.tags)}

Supported validation libraries:

  • msgspec.Struct (default, zero-copy, fastest)
  • Pydantic v2 BaseModel
  • dataclasses

Dependency Injection

Register values or factories and inject by parameter name:

from spikard import Spikard
from spikard.di import Provide

app = Spikard()
app.provide("config", {"db_url": "postgresql://localhost/app"})
app.provide("db", Provide(lambda config: f"pool({config['db_url']})", depends_on=["config"], singleton=True))

@app.get("/stats")
async def stats(config: dict[str, str], db: str):
    return {"db": db, "env": config["db_url"]}
  • TypedDict
  • NamedTuple
  • attrs classes

With Pydantic v2:

from pydantic import BaseModel, EmailStr

class User(BaseModel):
    name: str
    email: EmailStr

@app.post("/users")
async def create_user(user: User):
    return user.model_dump()

With dataclasses:

from dataclasses import dataclass

@dataclass
class Product:
    name: str
    price: float

@app.post("/products")
async def create_product(product: Product):
    return product

With plain JSON Schema dict:

user_schema = {
    "type": "object",
    "properties": {
        "name": {"type": "string"},
        "email": {"type": "string", "format": "email"}
    },
    "required": ["name", "email"]
}

@app.post("/users", request_schema=user_schema)
async def create_user(user: dict):
    # user is validated against schema
    return user

File Uploads

from spikard import UploadFile

@app.post("/upload")
async def upload_file(file: UploadFile):
    # Use aread() for async file reading
    content = await file.aread()
    return {
        "filename": file.filename,
        "size": file.size,
        "content_type": file.content_type,
        "content_length": len(content)
    }

Custom Responses

from spikard import Response

@app.post("/users")
async def create_user(user: User) -> Response:
    return Response(
        content=user,
        status_code=201,
        headers={"X-Custom": "value"}
    )

Streaming Responses

from spikard import StreamingResponse

async def generate_data():
    for i in range(10):
        yield f"data: {i}\n".encode()

@app.get("/stream")
async def stream():
    return StreamingResponse(generate_data())

Configuration

from spikard import Spikard, ServerConfig, CompressionConfig, RateLimitConfig

config = ServerConfig(
    host="0.0.0.0",
    port=8080,
    workers=4,
    enable_request_id=True,
    max_body_size=10 * 1024 * 1024,  # 10 MB
    request_timeout=30,
    compression=CompressionConfig(
        gzip=True,
        brotli=True,
        quality=6
    ),
    rate_limit=RateLimitConfig(
        per_second=100,
        burst=200
    )
)

app = Spikard(config=config)

Middleware Configuration

Compression:

from spikard import CompressionConfig

compression = CompressionConfig(
    gzip=True,          # Enable gzip
    brotli=True,        # Enable brotli
    min_size=1024,      # Min bytes to compress
    quality=6           # 0-11 for brotli, 0-9 for gzip
)

Rate Limiting:

from spikard import RateLimitConfig

rate_limit = RateLimitConfig(
    per_second=100,     # Max requests per second
    burst=200,          # Burst allowance
    ip_based=True       # Per-IP rate limiting
)

JWT Authentication:

from spikard import JwtConfig

jwt = JwtConfig(
    secret="your-secret-key",
    algorithm="HS256",  # HS256, HS384, HS512, RS256, etc.
    audience=["api.example.com"],
    issuer="auth.example.com",
    leeway=30  # seconds
)

Static Files:

from spikard import StaticFilesConfig

static = StaticFilesConfig(
    directory="./public",
    route_prefix="/static",
    index_file=True,
    cache_control="public, max-age=3600"
)

config = ServerConfig(static_files=[static])

OpenAPI Documentation:

from spikard.config import OpenApiConfig

openapi = OpenApiConfig(
    enabled=True,
    title="My API",
    version="1.0.0",
    description="API documentation",
    swagger_ui_path="/docs",
    redoc_path="/redoc"
)

config = ServerConfig(openapi=openapi)

Lifecycle Hooks

@app.on_request
async def log_request(request):
    print(f"{request.method} {request.path}")
    return request  # Must return request to continue

@app.pre_validation
async def check_auth(request):
    token = request.headers.get("Authorization")
    if not token:
        return Response({"error": "Unauthorized"}, status_code=401)
    return request

@app.pre_handler
async def rate_check(request):
    # Additional checks before handler
    return request

@app.on_response
async def add_headers(response):
    response.headers["X-Frame-Options"] = "DENY"
    return response

@app.on_error
async def log_error(response):
    print(f"Error: {response.status_code}")
    return response

WebSockets

from spikard import Spikard, websocket

app = Spikard()

@websocket("/ws")
async def chat_handler(message: dict) -> dict | None:
    """Handle incoming WebSocket messages."""
    print(f"Received: {message}")
    # Echo back the message
    return {"echo": message}

Server-Sent Events (SSE)

from spikard import Spikard, sse
import asyncio

app = Spikard()

@sse("/events")
async def event_stream():
    """Stream events to connected clients."""
    for i in range(10):
        await asyncio.sleep(1)
        yield {
            "count": i,
            "message": f"Event {i}",
            "timestamp": i
        }

Background Tasks

from spikard import background

async def heavy_processing(data: dict):
    """Async function that runs after response is sent."""
    # Heavy processing here
    pass

@app.post("/process")
async def process_data(data: dict):
    # Schedule async function to run in background
    background.run(heavy_processing(data))
    return {"status": "processing"}

Testing

from spikard import TestClient
import pytest

@pytest.fixture
def client():
    return TestClient(app)

@pytest.mark.asyncio
async def test_get_user(client):
    response = await client.get("/users/123")
    assert response.status_code == 200
    data = response.json()
    assert data["id"] == 123

@pytest.mark.asyncio
async def test_create_user(client):
    response = await client.post("/users", json={
        "name": "Alice",
        "email": "alice@example.com"
    })
    assert response.status_code == 201

WebSocket Testing

import json

@pytest.mark.asyncio
async def test_websocket(client):
    async with client.websocket("/ws") as ws:
        await ws.send(json.dumps({"message": "hello"}))
        response_text = await ws.recv()
        response = json.loads(response_text)
        assert response["echo"]["message"] == "hello"

SSE Testing

@pytest.mark.asyncio
async def test_sse(client):
    async with client.sse("/events") as sse:
        events = []
        async for event in sse:
            events.append(event.data)
            if len(events) >= 3:
                break
        assert len(events) == 3

Type Support

Spikard automatically extracts JSON schemas from:

  • msgspec.Struct (recommended, fastest)
  • Pydantic v2 BaseModel
  • dataclasses
  • TypedDict
  • NamedTuple

All compile to JSON Schema for validation and OpenAPI generation.

Performance

vs FastAPI

Comprehensive comparison across 18 real-world workloads on Apple M4 Pro (100 concurrent connections):

Framework Avg Throughput Mean Latency Difference
Spikard 35,779 req/s 7.44ms baseline
FastAPI 12,776 req/s 7.90ms -64% slower

Spikard is 2.8x faster than FastAPI with statistically significant improvements (p < 0.05).

Why Spikard is Faster

Rust-Powered Core:

  • HTTP server built on Tokio and Hyper
  • Tower middleware for zero-overhead routing
  • No Python GIL contention for HTTP layer

Zero-Copy Optimizations:

  • Direct PyO3 type construction (no JSON string serialization)
  • Eliminates 30-40% conversion overhead vs serialize-then-parse
  • msgspec integration for validation without extra allocations

Async-First Design:

  • pyo3_async_runtimes for efficient coroutine handling
  • Single event loop initialization per worker
  • GIL release before awaiting Rust futures

Running the Server

# Development
app.run(host="127.0.0.1", port=8000)

# Production with multiple workers
config = ServerConfig(
    host="0.0.0.0",
    port=8080,
    workers=4
)
app.run(config=config)

Examples

The examples directory contains comprehensive demonstrations:

Python-specific examples:

API Schemas (language-agnostic, can be used with code generation):

See examples/README.md for code generation instructions.

Documentation

Ecosystem Links

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

spikard-0.3.1.tar.gz (309.3 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

spikard-0.3.1-cp310-abi3-win_amd64.whl (5.6 MB view details)

Uploaded CPython 3.10+Windows x86-64

spikard-0.3.1-cp310-abi3-manylinux_2_34_x86_64.whl (5.5 MB view details)

Uploaded CPython 3.10+manylinux: glibc 2.34+ x86-64

spikard-0.3.1-cp310-abi3-macosx_14_0_arm64.whl (5.0 MB view details)

Uploaded CPython 3.10+macOS 14.0+ ARM64

File details

Details for the file spikard-0.3.1.tar.gz.

File metadata

  • Download URL: spikard-0.3.1.tar.gz
  • Upload date:
  • Size: 309.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for spikard-0.3.1.tar.gz
Algorithm Hash digest
SHA256 ecb30b47a6f705c27229f224a592be3d278965a2c86e0b3272d21d4191eb39b7
MD5 49c93860ea5b0c3c7c4021fc057eb1c5
BLAKE2b-256 17bc21b9b2765b27496b9f2a754c6224e3071a623482fa4eca69a5c5f3d71e3f

See more details on using hashes here.

Provenance

The following attestation bundles were made for spikard-0.3.1.tar.gz:

Publisher: publish.yaml on Goldziher/spikard

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file spikard-0.3.1-cp310-abi3-win_amd64.whl.

File metadata

  • Download URL: spikard-0.3.1-cp310-abi3-win_amd64.whl
  • Upload date:
  • Size: 5.6 MB
  • Tags: CPython 3.10+, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for spikard-0.3.1-cp310-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 eb824fc8251e76024e6799df242f552ae6f0c86b954d17ef7d3aca31f6e735fb
MD5 36e54a063a2462df006dbe93e53cd5e3
BLAKE2b-256 3a768beedf180b804ca0d401b31bc4d7a22f6d11d551b9fe864a0928f83a3fe3

See more details on using hashes here.

Provenance

The following attestation bundles were made for spikard-0.3.1-cp310-abi3-win_amd64.whl:

Publisher: publish.yaml on Goldziher/spikard

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file spikard-0.3.1-cp310-abi3-manylinux_2_34_x86_64.whl.

File metadata

File hashes

Hashes for spikard-0.3.1-cp310-abi3-manylinux_2_34_x86_64.whl
Algorithm Hash digest
SHA256 9b0119b7eb7fc0f9ead630e8f5558ad366bf25ab5dae3a03df3774a4fa8b48f2
MD5 cb4fea02bd968ed74bbf3cfbeb4f0a24
BLAKE2b-256 55646ee787dc49f498e9e6d5f0852d864e225fd257e122356ab1d10836585fe1

See more details on using hashes here.

Provenance

The following attestation bundles were made for spikard-0.3.1-cp310-abi3-manylinux_2_34_x86_64.whl:

Publisher: publish.yaml on Goldziher/spikard

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file spikard-0.3.1-cp310-abi3-macosx_14_0_arm64.whl.

File metadata

File hashes

Hashes for spikard-0.3.1-cp310-abi3-macosx_14_0_arm64.whl
Algorithm Hash digest
SHA256 ba444dd54b6432400a33e9935cbb8e58d48ff9142cc52e49c441c27eb312ccee
MD5 af5c3065d174926dd219f279537efa54
BLAKE2b-256 03091aa2b39a102844c145d59783e79103b3f0a13d29415f8033e2935b78d1b5

See more details on using hashes here.

Provenance

The following attestation bundles were made for spikard-0.3.1-cp310-abi3-macosx_14_0_arm64.whl:

Publisher: publish.yaml on Goldziher/spikard

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page