Skip to content

Routing Basics

Routing is uniform across bindings: create an app, register routes with typed parameters, and return typed responses.

Declare routes

from spikard import Spikard
from msgspec import Struct


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

app = Spikard()

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

@app.post("/users")
async def create_user(user: User) -> User:
    return user
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,
);
require "spikard"

app = Spikard::App.new

app.get("/health") { |_params, _query, _body| { status: "ok" } }
app.post("/users") { |_params, _query, body| body }
<?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());
use spikard::prelude::*;

let mut app = App::new();

app.route(get("/health"), |_ctx: Context| async { Ok(Json(json!({"status": "ok"}))) })?;
app.route(post("/users"), |ctx: Context| async move {
    let user: serde_json::Value = ctx.json()?;
    Ok(Json(user))
})?;

Path and query params

from spikard import Spikard
from msgspec import Struct

app = Spikard()


class OrderResponse(Struct):
    id: int
    details: bool


@app.get("/orders/{order_id:int}")
async def get_order(order_id: int, include_details: bool = False) -> OrderResponse:
    return OrderResponse(id=order_id, details=include_details)
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 };
  },
);
require "spikard"

app = Spikard::App.new

app.get("/orders/:order_id") do |params, query, _body|
  {
    id: params[:order_id].to_i,
    details: query["details"] == "true",
  }
end
<?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.