Service Design
Guidelines for designing effective WASI HTTP handlers with mik-sdk.
Required Imports
Section titled “Required Imports”All examples assume these imports:
#[allow(warnings)]mod bindings;
use bindings::exports::mik::core::handler::{self, Guest, Response};use mik_sdk::prelude::*;Architecture Principles
Section titled “Architecture Principles”Handlers Are Stateless
Section titled “Handlers Are Stateless”WASM handlers don’t maintain state between requests. Design accordingly:
// BAD: Trying to cache in memorystatic mut CACHE: Option<HashMap<String, User>> = None;
// GOOD: Use external services for statefn get_user(path: Id, _req: &Request) -> Response { // Check cache service let cached = fetch!(GET format!("http://cache:8080/users/{}", path.as_str())) .send();
if let Ok(resp) = cached { if resp.is_success() { return ok!({ "data": resp.json() }); } }
// Fetch from database // ...}Single Responsibility
Section titled “Single Responsibility”Each handler should do one thing well:
// GOOD: Clear, focused handlersroutes! { GET "/users" => list_users(query: ListQuery), GET "/users/{id}" => get_user(path: Id), POST "/users" => create_user(body: CreateInput), PUT "/users/{id}" => update_user(path: Id, body: UpdateInput), DELETE "/users/{id}" => delete_user(path: Id),}
// BAD: One handler doing too muchroutes! { POST "/api" => handle_everything(body: GenericInput),}Thin Handlers, Rich Domain
Section titled “Thin Handlers, Rich Domain”Keep handlers thin; move logic to pure functions:
// Business logic (testable, reusable)mod domain { pub fn validate_email(email: &str) -> bool { email.contains('@') && email.contains('.') }
pub fn format_user_name(first: &str, last: &str) -> String { format!("{} {}", first.trim(), last.trim()) }
pub fn calculate_age(birth_year: i32, current_year: i32) -> i32 { current_year - birth_year }}
// Handler (thin orchestration layer)fn create_user(body: CreateUserInput, _req: &Request) -> Response { guard!(domain::validate_email(&body.email), 400, "Invalid email");
let name = domain::format_user_name(&body.first_name, &body.last_name); let id = random::uuid();
// ... create user created!(format!("/users/{}", id), { "id": id, "name": name })}Request Handling
Section titled “Request Handling”Validate Early
Section titled “Validate Early”Validate inputs at the start of handlers:
fn create_order(body: OrderInput, req: &Request) -> Response { // Validate first guard!(body.items.len() > 0, 400, "Order must have items"); guard!(body.items.len() <= 100, 400, "Too many items");
for item in &body.items { guard!(item.quantity > 0, 400, "Quantity must be positive"); guard!(item.quantity <= 1000, 400, "Quantity too large"); }
// Then process // ...}Use Typed Inputs
Section titled “Use Typed Inputs”Prefer typed inputs over manual parsing:
// GOOD: Typed, validated inputs#[derive(Type)]pub struct CreateUserInput { #[field(min = 1, max = 100)] pub name: String, #[field(format = "email")] pub email: String,}
fn create_user(body: CreateUserInput, _req: &Request) -> Response { // body is already parsed and validated ok!({ "name": body.name })}
// AVOID: Manual parsing when typed inputs work betterfn create_user_manual(req: &Request) -> Response { let json = ensure!(req.json(), 400, "Invalid JSON"); let name = ensure!(json.path_str(&["name"]), 400, "name required"); // ... more manual validation}Handle Missing Data Gracefully
Section titled “Handle Missing Data Gracefully”fn get_user(path: Id, _req: &Request) -> Response { // Query database let (sql, params) = sql_read!(users { filter: { id: path.as_str() }, limit: 1, });
// Handle not found // let user = db.query_one(sql, params); // let user = ensure!(user, 404, "User not found");
ok!({ "id": path.as_str() })}Response Patterns
Section titled “Response Patterns”Consistent Response Structure
Section titled “Consistent Response Structure”Use consistent response formats:
// Success responsesfn get_item(path: Id, _req: &Request) -> Response { ok!({ "data": { "id": path.as_str(), "name": "Item" } })}
fn list_items(query: ListQuery, _req: &Request) -> Response { ok!({ "data": [], "meta": { "page": query.page, "limit": query.limit, "total": 0 } })}
// Error responses use RFC 7807 automaticallyfn handler(_req: &Request) -> Response { error! { status: 400, title: "Validation Error", detail: "Field 'name' is required" }}Use Appropriate Status Codes
Section titled “Use Appropriate Status Codes”// 200 OK - Success with dataok!({ "data": result })
// 201 Created - Resource createdcreated!("/items/123", { "id": "123" })
// 202 Accepted - Async operation startedaccepted!()
// 204 No Content - Success, no datano_content!()
// 400 Bad Request - Client errorbad_request!("Invalid input")
// 404 Not Found - Resource doesn't existnot_found!("Item not found")
// 409 Conflict - State conflictconflict!("Item already exists")
// 500+ - Server errors (use sparingly)error! { status: 503, title: "Service Unavailable", detail: "..." }Database Patterns
Section titled “Database Patterns”One Query Per Table
Section titled “One Query Per Table”Avoid N+1 queries:
// BAD: N+1 queriesfn list_posts(_req: &Request) -> Response { // let posts = get_posts(); // for post in &posts { // let author = get_user(post.author_id); // N queries! // }}
// GOOD: Batched loadingfn list_posts(_req: &Request) -> Response { let (sql, params) = sql_read!(posts { select: [id, title, author_id], limit: 20, }); // let posts = db.query(sql, params);
// Batch load authors // let author_ids = ids!(posts, author_id); // let (sql, params) = sql_read!(users { // filter: { id: { $in: author_ids } }, // }); // let authors = db.query(sql, params);
ok!({})}Use Cursor Pagination for Large Datasets
Section titled “Use Cursor Pagination for Large Datasets”// Offset pagination - OK for small datasetsfn list_items_offset(query: PageQuery, _req: &Request) -> Response { let (sql, params) = sql_read!(items { order: [id], page: query.page, limit: query.limit, }); ok!({})}
// Cursor pagination - better for large datasetsfn list_items_cursor(query: CursorQuery, _req: &Request) -> Response { let (sql, params) = sql_read!(items { order: [-created_at, -id], after: query.cursor.as_deref(), limit: query.limit, }); ok!({})}Error Handling
Section titled “Error Handling”Use Guard and Ensure
Section titled “Use Guard and Ensure”fn process_order(body: OrderInput, _req: &Request) -> Response { // Guard for validation guard!(body.items.len() > 0, 400, "No items in order"); guard!(body.total > 0.0, 400, "Invalid total");
// Ensure for unwrapping let user = ensure!(find_user(&body.user_id), 404, "User not found"); let inventory = ensure!(check_inventory(&body.items), 409, "Items unavailable");
ok!({ "status": "processed" })}Log Errors
Section titled “Log Errors”fn handler(req: &Request) -> Response { let result = process_request(req);
match result { Ok(data) => ok!({ "data": data }), Err(err) => { log!(error, "request failed", error: format!("{:?}", err), path: req.path_without_query() );
error! { status: 500, title: "Internal Error", detail: "An unexpected error occurred" } } }}Observability
Section titled “Observability”Structured Logging
Section titled “Structured Logging”fn handler(req: &Request) -> Response { let trace_id = req.trace_id_or("unknown"); let start = time::now_millis();
log!(info, "request started", trace_id: trace_id, method: format!("{:?}", req.method()), path: req.path_without_query() );
// ... process request
let elapsed = time::now_millis() - start;
log!(info, "request completed", trace_id: trace_id, duration_ms: elapsed, status: 200 );
ok!({})}Propagate Trace IDs
Section titled “Propagate Trace IDs”fn call_downstream(req: &Request) -> Response { let trace_id = req.trace_id_or(""); let response = fetch!(GET "http://service:8080/data") .with_trace_id(if trace_id.is_empty() { None } else { Some(trace_id) }) .send()?;
ok!({ "status": response.status() })}