Skip to content

Migrating from v0.15 to v0.16

v0.16 aligns the polyglot surface to one coherent API across all language bindings. Every binding now exposes the same handler shapes, lifecycle hooks, error hierarchy, and testing utilities. If you're upgrading from v0.15, review the breaking changes for your language.

Breaking changes by binding

Path parameter syntax

v0.15 accepted {id} in route paths; v0.16 now parses type constraints.

  • {id} — still works, treated as string
  • {id:int} — type constraint, coerced to integer (v0.16+)
  • {id:uuid} — validates UUID format (v0.16+)
  • :id — shorthand for {id}, works in all bindings

The loc field on validation errors is now correctly scoped:

  • Path parameter mismatch: ["path", "id"] (was incorrectly ["query", "id"] in v0.15)
  • Query parameter mismatch: ["query", "param_name"]
  • Request body mismatch: ["body", "field.nested.path"]
  • Header validation: ["header", "X-Custom-Header"]
  • Cookie validation: ["cookie", "session_id"]

Exception hierarchy (all languages)

v0.16 adds typed exception classes across all bindings. Catch specific errors instead of checking status codes:

  • NotFoundError → 404
  • ValidationError → 422 (with loc: [scope, name] fields)
  • AuthenticationError → 401
  • AuthorizationError → 403
  • RateLimitExceededError → 429
  • InternalError → 500

Python

from spikard import App, ValidationError, NotFoundError

app = App()

@app.get("/users/{id:int}")
async def get_user(id: int):
    if id < 1:
        raise NotFoundError("User not found")
    # ...

# In test or calling code:
try:
    # make request
except ValidationError as e:
    for field_error in e.errors:
        print(f"{field_error.loc}: {field_error.message}")
except NotFoundError as e:
    print(f"404: {e.message}")

TypeScript / Node

import { App, ValidationError, NotFoundError } from "@spikard/node";

const app = new App();

app.get("/users/:id", async (c) => {
  const id = Number(c.req.param("id"));
  if (id < 1) {
    throw new NotFoundError("User not found");
  }
  return c.json({ id });
});

// In calling code:
try {
  // make request
} catch (e) {
  if (e instanceof ValidationError) {
    e.errors.forEach((err) => {
      console.log(`${err.loc.join(".")}: ${err.message}`);
    });
  } else if (e instanceof NotFoundError) {
    console.log(`404: ${e.message}`);
  }
}

Ruby

require "spikard"

app = Spikard::App.new

app.get("/users/:id") do |req|
  id = req.params[:id].to_i
  raise Spikard::NotFoundError, "User not found" if id < 1
  { id: id }
end

# In calling code:
begin
  # make request
rescue Spikard::ValidationError => e
  e.errors.each do |err|
    puts "#{err.loc.join(".")}: #{err.message}"
  end
rescue Spikard::NotFoundError => e
  puts "404: #{e.message}"
end

PHP

use Spikard\App;
use Spikard\Errors\ValidationError;
use Spikard\Errors\NotFoundError;

$app = new App();

$app->get('/users/{id}', function($req) {
    $id = (int)$req->param('id');
    if ($id < 1) {
        throw new NotFoundError('User not found');
    }
    return ['id' => $id];
});

// In calling code:
try {
    // make request
} catch (ValidationError $e) {
    foreach ($e->errors() as $err) {
        echo implode(".", $err->loc) . ": " . $err->message() . "\n";
    }
} catch (NotFoundError $e) {
    echo "404: " . $e->getMessage() . "\n";
}

Elixir

defmodule MyApp do
  use Spikard.Router

  get "/users/:id" do
    id = String.to_integer(id)
    if id < 1 do
      raise Spikard.NotFoundError, "User not found"
    end
    send_resp(conn, 200, Jason.encode!(%{id: id}))
  end
end

# In calling code:
try do
  # make request
rescue
  e in Spikard.ValidationError ->
    Enum.each(e.errors, fn err ->
      IO.puts("#{Enum.join(err.loc, ".")}: #{err.message}")
    end)

  e in Spikard.NotFoundError ->
    IO.puts("404: #{e.message}")
end

Handler signatures

v0.16 standardizes handler syntax across languages with idiomatic variants. v0.15 handlers require rewriting.

Python

v0.15 used kwargs unpacking; v0.16 uses FastAPI-style signature introspection with decorators.

# v0.15
def get_user(**kwargs):
    id = kwargs.get("path_params", {}).get("id")
    return {"id": id}

app.get("/users/{id}", get_user)

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

TypeScript / Node

v0.15 passed raw request; v0.16 uses Hono-style Context with typed accessors.

// v0.15
app.get("/users/{id}", (req) => {
  const id = req.path_params.id;
  return { id };
});

// v0.16
app.get("/users/:id", (c) => {
  const id = Number(c.req.param("id"));
  return c.json({ id });
});

Ruby

v0.15 used block parameters; v0.16 standardizes on Request object with method accessors.

# v0.15
app.get("/users/{id}") { |params| { id: params["id"] } }

# v0.16
app.get("/users/:id") do |req|
  id = req.params[:id].to_i
  { id: id }
end

Go

v0.15 used bare []byte handlers; v0.16 uses http.HandlerFunc with chi-style route binding.

// v0.15
app.Get("/users/{id}", func(input []byte) ([]byte, error) {
    var id string
    json.Unmarshal(input, &id)
    return json.Marshal(map[string]any{"id": id})
})

// v0.16
app.Get("/users/{id}", func(w http.ResponseWriter, r *http.Request) {
    id := spikard.PathParam(r, "id")
    json.NewEncoder(w).Encode(map[string]any{"id": id})
})

Java

v0.15 used raw String handlers; v0.16 uses Javalin-style Context with typed accessors.

// v0.15
app.get("/users/{id}", req -> String.valueOf(req.pathParam("id")));

// v0.16
app.get("/users/{id}", ctx -> ctx.json(
    Map.of("id", ctx.pathParam("id", Integer.class))
));

New features available in v0.16

Every binding now exposes:

TestClient — in-process integration testing without binding a TCP socket:

from spikard import App, TestClient

app = App()
client = TestClient(app)

response = client.get("/users/1")
assert response.status == 200

Type-safe exceptions — all documented exceptions available in every binding for precise error handling.

Extended ServerConfig — set timeouts, enable compression, configure CORS, rate limiting, and static files via configuration objects passed to app.config().

App construction

v0.16 standardizes the top-level class name. Every binding exports App.

# v0.16 and later
from spikard import App

app = App()
// v0.16 and later
import { App } from "@spikard/node";

const app = new App();

Configuration

v0.16 fixes configuration forwarding in Node, Go, Java, and C#. The ServerConfig is now correctly passed to the native layer in all bindings.

from spikard import App, ServerConfig

config = ServerConfig(
    host="0.0.0.0",
    port=3000,
    workers=4,
    request_timeout=30,
    enable_cors=True,
)
app = App().config(config)
app.run()
import { App, ServerConfig } from "@spikard/node";

const config: ServerConfig = {
  host: "0.0.0.0",
  port: 3000,
  workers: 4,
  requestTimeout: 30,
  enableCors: true,
};
const app = new App().config(config);
app.run();

Deprecated APIs

No APIs were explicitly deprecated in v0.16; the breaking changes are listed above. v0.15 code will require rewriting to match the new handler shapes.

Edit this page on GitHub