Skip to content

Routing

mik-sdk provides a declarative, type-safe routing system with automatic extraction of path parameters, query strings, and request bodies.

Every handler file needs these imports:

#[allow(warnings)]
mod bindings;
use bindings::exports::mik::core::handler::{self, Guest, Response};
use mik_sdk::prelude::*;
  • bindings - Generated by cargo-component from your WIT files
  • Guest - The trait your handler implements (generated by the routes! macro)
  • Response - The response type returned by handlers
  • mik_sdk::prelude::* - All SDK types and macros (Request, Path, Query, Type, etc.)
routes! {
GET "/" => home,
GET "/users" => list_users,
POST "/users" => create_user,
GET "/users/{id}" => get_user,
PUT "/users/{id}" => update_user,
DELETE "/users/{id}" => delete_user,
}
MethodUsage
GETRead resources
POSTCreate resources
PUTReplace resources
PATCHPartial update
DELETERemove resources
HEADHeaders only
OPTIONSCORS preflight
routes! {
// Static paths
GET "/" => home,
GET "/api/health" => health,
// Single parameter
GET "/users/{id}" => get_user,
// Multiple parameters
GET "/orgs/{org_id}/users/{user_id}" => get_org_user,
// Alternative paths (both map to same handler)
GET "/" | "" => home,
}

Use #[derive(Path)] for URL path parameters:

#[derive(Path)]
pub struct UserPath {
pub id: String, // Matches {id} in route
}
#[derive(Path)]
pub struct OrgUserPath {
pub org_id: String, // Matches {org_id}
pub user_id: String, // Matches {user_id}
}
routes! {
GET "/users/{id}" => get_user(path: UserPath),
GET "/orgs/{org_id}/users/{user_id}" => get_org_user(path: OrgUserPath),
}
fn get_user(path: UserPath, _req: &Request) -> Response {
ok!({ "id": path.id })
}

Use #[derive(Query)] for query string parameters:

#[derive(Query)]
pub struct ListQuery {
// Optional parameter
pub search: Option<String>,
// With default value
#[field(default = 1)]
pub page: u32,
// With default and max constraint
#[field(default = 20, max = 100)]
pub limit: u32,
}
routes! {
GET "/users" => list_users(query: ListQuery),
}
fn list_users(query: ListQuery, _req: &Request) -> Response {
ok!({
"search": query.search,
"page": query.page,
"limit": query.limit
})
}

Query strings are parsed from the URL:

/users?search=alice&page=2&limit=50

Use #[derive(Type)] for JSON request bodies:

#[derive(Type)]
pub struct CreateUserInput {
#[field(min = 1, max = 100)]
pub name: String,
#[field(format = "email")]
pub email: String,
// Optional field
pub age: Option<i32>,
}
routes! {
POST "/users" => create_user(body: CreateUserInput),
}
fn create_user(body: CreateUserInput, _req: &Request) -> Response {
ok!({
"name": body.name,
"email": body.email,
"age": body.age
})
}

Handlers can receive multiple typed inputs:

routes! {
PUT "/users/{id}" => update_user(path: Id, body: UpdateInput),
GET "/orgs/{org_id}/users" => list_org_users(path: OrgPath, query: ListQuery),
}
fn update_user(path: Id, body: UpdateInput, _req: &Request) -> Response {
ok!({
"id": path.as_str(),
"updated_name": body.name
})
}

Optionally declare response types for documentation:

#[derive(Type)]
pub struct User {
pub id: String,
pub name: String,
pub email: String,
}
routes! {
GET "/users/{id}" => get_user(path: Id) -> User,
GET "/users" => list_users(query: ListQuery) -> Vec<User>,
}

Response types are used for:

  • OpenAPI schema generation (via cargo test __mik_write_schema)
  • Documentation

Use #[field(...)] to add constraints and metadata:

#[derive(Type)]
pub struct CreatePostInput {
// String constraints
#[field(min = 1, max = 200)]
pub title: String,
// Rename JSON field
#[field(rename = "bodyContent")]
pub body: String,
// Format hint (for OpenAPI)
#[field(format = "email")]
pub author_email: String,
// Pattern validation (for OpenAPI)
#[field(pattern = "^[a-z0-9-]+$")]
pub slug: String,
// Documentation
#[field(docs = "Tags for categorization")]
pub tags: Vec<String>,
}
#[derive(Query)]
pub struct PaginationQuery {
// Default value (Query only)
#[field(default = 1)]
pub page: u32,
// Default with max constraint
#[field(default = 20, max = 100)]
pub limit: u32,
}
AttributeTypesDescription
minString, Vec, numbersMinimum length/value/items
maxString, Vec, numbersMaximum length/value/items
defaultQuery fieldsDefault if missing
formatStringOpenAPI format hint
patternStringRegex pattern
renameAnyJSON field name
docsAnyOpenAPI description

The routes! macro generates OpenAPI schema that can be extracted via test:

Terminal window
# Generate openapi.json
cargo test __mik_write_schema
# The schema is written to openapi.json in your project root

The schema is generated at test time only and is not included in the WASM binary.

All handlers have this signature:

fn handler_name(
/* typed inputs */,
req: &Request,
) -> Response

The Request parameter is always available for accessing:

  • Headers
  • Raw body
  • Content-Type checks
  • Form data
fn create_user(body: CreateInput, req: &Request) -> Response {
// Access raw request
let auth = req.header_or("authorization", "");
let trace_id = req.trace_id_or("");
// Use typed input
ok!({ "name": body.name })
}

Parsing errors are automatically returned as RFC 7807 responses:

{
"type": "urn:problem:validation",
"title": "Validation Error",
"status": 400,
"detail": "Missing required field",
"errors": [{ "field": "name", "message": "field is required" }]
}

For custom error handling, use the DX macros:

fn get_user(path: Id, _req: &Request) -> Response {
// Early return if condition fails
guard!(!path.as_str().is_empty(), 400, "ID required");
// Unwrap or return error
let user = ensure!(find_user(path.as_str()), 404, "Not found");
ok!({ "user": user })
}