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 asstring{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→ 404ValidationError→ 422 (withloc: [scope, name]fields)AuthenticationError→ 401AuthorizationError→ 403RateLimitExceededError→ 429InternalError→ 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.
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.