Skip to content

Configuration

Spikard favors code-based configuration with environment overrides. The same knobs exist regardless of binding.

Basic ServerConfig

Create and configure your server with explicit settings:

from spikard import Spikard
from spikard.config import ServerConfig

config = ServerConfig(
    host="0.0.0.0",
    port=8080,
    workers=4,
    request_timeout=60,
    max_body_size=5 * 1024 * 1024,  # 5MB
)

app = Spikard(config=config)

@app.get("/health")
async def health():
    return {"status": "ok"}

if __name__ == "__main__":
    app.run()
import { Spikard, runServer } from "spikard";
import type { ServerConfig, Request } from "spikard";

const config: ServerConfig = {
  host: "0.0.0.0",
  port: 8080,
  workers: 4,
  requestTimeout: 60,
  maxBodySize: 5 * 1024 * 1024,  // 5MB
};

const app = new Spikard();

app.addRoute(
  { method: "GET", path: "/health", handler_name: "health", is_async: true },
  async (req: Request) => ({ status: "ok" }),
);

if (require.main === module) {
  runServer(app, config);
}
require "spikard"

config = Spikard::ServerConfig.new(
  host: "0.0.0.0",
  port: 8080,
  workers: 4,
  request_timeout: 60,
  max_body_size: 5 * 1024 * 1024  # 5MB
)

app = Spikard::App.new(config: config)

app.get("/health") do
  { status: "ok" }
end

app.run
<?php

declare(strict_types=1);

use Spikard\App;
use Spikard\Attributes\Get;
use Spikard\Config\ServerConfig;
use Spikard\Http\Response;

final class HealthController
{
    #[Get('/health')]
    public function health(): Response
    {
        return Response::json(['status' => 'ok']);
    }
}

$config = ServerConfig::builder()
    ->withHost('0.0.0.0')
    ->withPort(8080)
    ->withWorkers(4)
    ->withRequestTimeout(60)
    ->withMaxBodySize(5 * 1024 * 1024)  // 5MB
    ->build();

$app = (new App($config))
    ->registerController(new HealthController());

$app->run();
use axum::response::Json;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use spikard::{get, App, ServerConfig};

#[derive(Serialize, Deserialize, JsonSchema)]
struct HealthResponse {
    status: String,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = ServerConfig::builder()
        .host("0.0.0.0")
        .port(8080)
        .workers(4)
        .request_timeout_secs(60)
        .max_body_size(5 * 1024 * 1024)  // 5MB
        .build();

    let mut app = App::with_config(config);

    app.route(get("/health"), |_ctx| async move {
        Ok(Json(HealthResponse { status: "ok".into() }).into())
    })?;

    app.run().await?;
    Ok(())
}

Environment Variable Overrides

Override config at runtime without changing code:

import os
from spikard import Spikard
from spikard.config import ServerConfig

config = ServerConfig(
    host=os.getenv("SPIKARD_HOST", "127.0.0.1"),
    port=int(os.getenv("SPIKARD_PORT", "8000")),
    workers=int(os.getenv("SPIKARD_WORKERS", "1")),
    request_timeout=int(os.getenv("SPIKARD_TIMEOUT", "30")),
)

app = Spikard(config=config)

# Keep secrets in env
api_key = os.getenv("API_KEY")
db_url = os.getenv("DATABASE_URL")
import { Spikard, runServer } from "spikard";
import type { ServerConfig } from "spikard";

const config: ServerConfig = {
  host: process.env.SPIKARD_HOST || "127.0.0.1",
  port: parseInt(process.env.SPIKARD_PORT || "8000"),
  workers: parseInt(process.env.SPIKARD_WORKERS || "1"),
  requestTimeout: parseInt(process.env.SPIKARD_TIMEOUT || "30"),
};

const app = new Spikard();

// Keep secrets in env
const apiKey = process.env.API_KEY;
const dbUrl = process.env.DATABASE_URL;

runServer(app, config);
require "spikard"

config = Spikard::ServerConfig.new(
  host: ENV.fetch("SPIKARD_HOST", "127.0.0.1"),
  port: ENV.fetch("SPIKARD_PORT", "8000").to_i,
  workers: ENV.fetch("SPIKARD_WORKERS", "1").to_i,
  request_timeout: ENV.fetch("SPIKARD_TIMEOUT", "30").to_i
)

app = Spikard::App.new(config: config)

# Keep secrets in env
api_key = ENV["API_KEY"]
db_url = ENV["DATABASE_URL"]
<?php

declare(strict_types=1);

use Spikard\App;
use Spikard\Config\ServerConfig;

$config = ServerConfig::builder()
    ->withHost(getenv('SPIKARD_HOST') ?: '127.0.0.1')
    ->withPort((int) (getenv('SPIKARD_PORT') ?: 8000))
    ->withWorkers((int) (getenv('SPIKARD_WORKERS') ?: 1))
    ->withRequestTimeout((int) (getenv('SPIKARD_TIMEOUT') ?: 30))
    ->build();

$app = new App($config);

// Keep secrets in env
$apiKey = getenv('API_KEY');
$dbUrl = getenv('DATABASE_URL');
use spikard::{App, ServerConfig};
use std::env;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Load from .env file if present
    dotenv::dotenv().ok();

    let config = ServerConfig::builder()
        .host(env::var("SPIKARD_HOST").unwrap_or_else(|_| "127.0.0.1".into()))
        .port(
            env::var("SPIKARD_PORT")
                .ok()
                .and_then(|p| p.parse().ok())
                .unwrap_or(8000),
        )
        .workers(
            env::var("SPIKARD_WORKERS")
                .ok()
                .and_then(|w| w.parse().ok())
                .unwrap_or(1),
        )
        .request_timeout_secs(
            env::var("SPIKARD_TIMEOUT")
                .ok()
                .and_then(|t| t.parse().ok())
                .unwrap_or(30),
        )
        .build();

    let app = App::with_config(config);

    // Keep secrets in env
    let api_key = env::var("API_KEY").ok();
    let db_url = env::var("DATABASE_URL").ok();

    app.run().await?;
    Ok(())
}

Run with: SPIKARD_PORT=8080 SPIKARD_WORKERS=4 python app.py

Production Configuration

Full production setup with compression, rate limiting, and monitoring:

import os
from spikard import Spikard
from spikard.config import (
    CompressionConfig,
    OpenApiConfig,
    RateLimitConfig,
    ServerConfig,
)

config = ServerConfig(
    host="0.0.0.0",
    port=8080,
    workers=4,
    request_timeout=60,
    max_body_size=10 * 1024 * 1024,

    # High-quality compression
    compression=CompressionConfig(
        gzip=True,
        brotli=True,
        min_size=1024,
        quality=6,
    ),

    # Protect against abuse
    rate_limit=RateLimitConfig(
        per_second=100,
        burst=200,
        ip_based=True,
    ),

    # Auto-generated docs
    openapi=OpenApiConfig(
        enabled=True,
        title="Production API",
        version="1.0.0",
    ),

    # Graceful shutdown
    graceful_shutdown=True,
    shutdown_timeout=30,
)

app = Spikard(config=config)
import { Spikard, runServer } from "spikard";
import type { ServerConfig } from "spikard";

const config: ServerConfig = {
  host: "0.0.0.0",
  port: 8080,
  workers: 4,
  requestTimeout: 60,
  maxBodySize: 10 * 1024 * 1024,

  // High-quality compression
  compression: {
    gzip: true,
    brotli: true,
    minSize: 1024,
    quality: 6,
  },

  // Protect against abuse
  rateLimit: {
    perSecond: 100,
    burst: 200,
    ipBased: true,
  },

  // Auto-generated docs
  openapi: {
    enabled: true,
    title: "Production API",
    version: "1.0.0",
  },

  // Graceful shutdown
  gracefulShutdown: true,
  shutdownTimeout: 30,
};

const app = new Spikard();
runServer(app, config);
require "spikard"

config = Spikard::ServerConfig.new(
  host: "0.0.0.0",
  port: 8080,
  workers: 4,
  request_timeout: 60,
  max_body_size: 10 * 1024 * 1024,

  # High-quality compression
  compression: Spikard::CompressionConfig.new(
    gzip: true,
    brotli: true,
    min_size: 1024,
    quality: 6
  ),

  # Protect against abuse
  rate_limit: Spikard::RateLimitConfig.new(
    per_second: 100,
    burst: 200,
    ip_based: true
  ),

  # Auto-generated docs
  openapi: Spikard::OpenApiConfig.new(
    enabled: true,
    title: "Production API",
    version: "1.0.0"
  ),

  # Graceful shutdown
  graceful_shutdown: true,
  shutdown_timeout: 30
)

app = Spikard::App.new(config: config)
<?php

declare(strict_types=1);

use Spikard\App;
use Spikard\Config\CompressionConfig;
use Spikard\Config\OpenApiConfig;
use Spikard\Config\RateLimitConfig;
use Spikard\Config\ServerConfig;

// Production error handling
error_reporting(E_ALL);
ini_set('display_errors', '0');
ini_set('log_errors', '1');

$config = ServerConfig::builder()
    ->withHost('0.0.0.0')
    ->withPort(8080)
    ->withWorkers(4)
    ->withRequestTimeout(60)
    ->withMaxBodySize(10 * 1024 * 1024)

    // High-quality compression
    ->withCompression(
        CompressionConfig::builder()
            ->withGzip(true)
            ->withBrotli(true)
            ->withMinSize(1024)
            ->withQuality(6)
            ->build()
    )

    // Protect against abuse
    ->withRateLimit(
        RateLimitConfig::builder()
            ->withPerSecond(100)
            ->withBurst(200)
            ->withIpBased(true)
            ->build()
    )

    // Auto-generated docs
    ->withOpenApi(
        OpenApiConfig::builder()
            ->withEnabled(true)
            ->withTitle('Production API')
            ->withVersion('1.0.0')
            ->build()
    )

    // Graceful shutdown
    ->withGracefulShutdown(true)
    ->withShutdownTimeout(30)
    ->build();

$app = new App($config);
use spikard::{
    App, CompressionConfig, OpenApiConfig, RateLimitConfig, ServerConfig,
};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Production logging setup
    tracing_subscriber::registry()
        .with(tracing_subscriber::fmt::layer().json())
        .with(tracing_subscriber::EnvFilter::from_default_env())
        .init();

    // Panic handling for production
    std::panic::set_hook(Box::new(|info| {
        tracing::error!("Panic occurred: {:?}", info);
    }));

    let config = ServerConfig::builder()
        .host("0.0.0.0")
        .port(8080)
        .workers(4)
        .request_timeout_secs(60)
        .max_body_size(10 * 1024 * 1024)

        // High-quality compression
        .compression(
            CompressionConfig::builder()
                .gzip(true)
                .brotli(true)
                .min_size(1024)
                .quality(6)
                .build(),
        )

        // Protect against abuse
        .rate_limit(
            RateLimitConfig::builder()
                .per_second(100)
                .burst(200)
                .ip_based(true)
                .build(),
        )

        // Auto-generated docs
        .openapi(
            OpenApiConfig::builder()
                .enabled(true)
                .title("Production API")
                .version("1.0.0")
                .build(),
        )

        // Graceful shutdown
        .graceful_shutdown(true)
        .shutdown_timeout_secs(30)
        .build();

    let app = App::with_config(config);
    app.run().await?;
    Ok(())
}

Server Settings

  • host / port – network binding for the HTTP server
  • workers – Tokio worker thread count; defaults to 1. Set explicitly (for example to CPU count) in production.
  • keep_alive / request_timeout – tune to match upstream load balancers
  • max_body_size – prevent memory exhaustion from large uploads

Middleware Defaults

  • Logging/tracing enabled by default with request IDs
  • CORS/compression configurable per app or per route
  • Add custom middleware to inject tenant data, auth, or feature flags

Validation Controls

  • Enable/disable request and response validation globally or per route
  • Provide JSON Schemas or rely on derived DTOs
  • Customize error formatting for clients

TLS and HTTP/2 (via Reverse Proxy)

Spikard listens on plain HTTP. Use a reverse proxy for TLS termination:

upstream spikard_backend {
    server 127.0.0.1:8080;
}

server {
    listen 443 ssl http2;
    server_name api.example.com;

    ssl_certificate /etc/ssl/certs/api.example.com.crt;
    ssl_certificate_key /etc/ssl/private/api.example.com.key;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;

    location / {
        proxy_pass http://spikard_backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
api.example.com {
    reverse_proxy localhost:8080
}
# docker-compose.yml
services:
  traefik:
    image: traefik:v2.10
    command:
      - --entrypoints.websecure.address=:443
      - --certificatesresolvers.myresolver.acme.tlschallenge=true
    ports:
      - "443:443"
    labels:
      - "traefik.http.routers.api.rule=Host(`api.example.com`)"
      - "traefik.http.routers.api.tls.certresolver=myresolver"
      - "traefik.http.services.api.loadbalancer.server.port=8080"

Verify It Works

curl http://localhost:8080/health
# Expected: {"status":"ok"}
# Swagger UI
open http://localhost:8080/docs

# Redoc
open http://localhost:8080/redoc

# Raw spec
curl http://localhost:8080/openapi.json
# Trigger rate limit
for i in {1..150}; do curl http://localhost:8080/health; done
# Expected: 429 Too Many Requests after burst limit

Use the Taskfile (task docs:serve) to iterate on examples locally.