Routing Basics¶
Routing is uniform across bindings: create an app, register routes with typed parameters, and return typed responses.
Declare routes¶
import { Spikard, type Request } from "spikard";
import { z } from "zod";
const UserSchema = z.object({ id: z.number(), name: z.string() });
type User = z.infer<typeof UserSchema>;
const app = new Spikard();
const health = async (): Promise<{ status: string }> => ({ status: "ok" });
const createUser = async (req: Request): Promise<User> => {
return UserSchema.parse(req.json());
};
app.addRoute(
{ method: "GET", path: "/health", handler_name: "health", is_async: true },
health,
);
app.addRoute(
{
method: "POST",
path: "/users",
handler_name: "createUser",
request_schema: UserSchema,
response_schema: UserSchema,
is_async: true,
},
createUser,
);
<?php
declare(strict_types=1);
use Spikard\App;
use Spikard\Attributes\Get;
use Spikard\Attributes\Post;
use Spikard\Attributes\Put;
use Spikard\Attributes\Delete;
use Spikard\Config\ServerConfig;
use Spikard\Http\Request;
use Spikard\Http\Response;
final class ResourceController
{
#[Get('/items')]
public function list(): Response
{
return Response::json(['items' => []]);
}
#[Post('/items')]
public function create(Request $request): Response
{
return Response::json($request->body, 201);
}
#[Put('/items/{id}')]
public function update(Request $request): Response
{
$id = (int) $request->pathParams['id'];
return Response::json(['id' => $id, 'updated' => true]);
}
#[Delete('/items/{id}')]
public function delete(Request $request): Response
{
return Response::json(null, 204);
}
}
$app = (new App(new ServerConfig(port: 8000)))
->registerController(new ResourceController());
Path and query params¶
import { Spikard, type Request } from "spikard";
interface OrderResponse {
id: number;
details: boolean;
}
const app = new Spikard();
app.addRoute(
{ method: "GET", path: "/orders/:order_id", handler_name: "getOrder", is_async: true },
async (req: Request): Promise<OrderResponse> => {
const id = Number(req.params["order_id"] ?? 0);
const details = req.query["details"] === "true";
return { id, details };
},
);
<?php
declare(strict_types=1);
use Spikard\App;
use Spikard\Attributes\Get;
use Spikard\Config\ServerConfig;
use Spikard\Http\Request;
use Spikard\Http\Response;
final class OrdersController
{
#[Get('/orders/{order_id}')]
public function show(Request $request): Response
{
// Path parameters
$orderId = (int) $request->pathParams['order_id'];
// Query parameters (array for multi-value support)
$includeDetails = ($request->queryParams['include_details'][0] ?? 'false') === 'true';
$limit = (int) ($request->queryParams['limit'][0] ?? '10');
return Response::json([
'id' => $orderId,
'details' => $includeDetails,
'limit' => $limit
]);
}
}
$app = (new App(new ServerConfig(port: 8000)))
->registerController(new OrdersController());
app.route(get("/orders/:order_id"), |ctx: Context| async move {
let order_id: i64 = ctx.path_param::<String>("order_id")?
.parse()
.map_err(|_| Error::BadRequest("order_id must be a valid number"))?;
#[derive(serde::Deserialize, Default)]
struct DetailsQuery {
details: Option<bool>,
}
let query: DetailsQuery = ctx.query().unwrap_or_default();
Ok(Json(json!({
"id": order_id,
"details": query.details.unwrap_or(false)
})))
})?;
Best practices¶
- Keep handlers small and pure; push IO into services.
- Prefer DTOs for shared schemas so codegen can derive OpenAPI/AsyncAPI.
- Use per-route middleware when sensitive endpoints need extra auth/logging.