struct CreateUserRequest {
name : String
email : String
} derive(ToJson, FromJson)
struct CreateUserResponse {
id : String
name : String
} derive(ToJson, FromJson)
let create_user_handler = ActionHandler(async fn(ctx) {
let req : CreateUserRequest = match ctx.decode_json() {
Some(req) => req
None => return ActionResult::bad_request("Invalid JSON payload")
}
if req.email == "" {
return ActionResult::validation_error("Email is required")
}
ActionResult::json(CreateUserResponse::{ id: "123", name: req.name })
})
let update_profile_handler = ActionHandler(async fn(ctx) {
let body = ctx.body
// ... parse and validate
if body == "" {
return ActionResult::validation_error("Profile payload is required")
}
ActionResult::json({ "message": "Profile updated" })
})
let delete_user_handler = ActionHandler(async fn(ctx) {
// Check authorization
let user_id = ctx.get_query("id")
// ...
// Return redirect on success
ActionResult::redirect("/users")
})// `sol generate` creates these factories from handler binding names:
// - create_user_handler -> @types.action_create_user()
// - submit_contact_handler -> @types.action_submit_contact()
// - add_todo_action -> @types.action_add_todo()
// - delete_user_handler -> @types.action_delete_user()
let registry = ActionRegistry::new(
allowed_origins=["https://example.com"]
)
.register(
ActionDef::from_key(@types.action_create_user(), create_user_handler)
)
.register(
ActionDef::from_key(@types.action_delete_user(), delete_user_handler)
.with_require_json(false)
)let app = @hono.Hono::new()
let app = register_actions(app, registry)let create_user_action : TypedActionKey[CreateUserRequest, CreateUserResponse] =
@types.action_create_user_typed()
let handle_create_user : (TypedActionResponse[CreateUserResponse]) -> Unit = fn(response) {
match response {
Success(data) => {
save_user_id(data.id)
}
Redirect(url) => {
// Handle redirect (usually automatic)
}
Error(status, message) => {
// Handle error
}
NetworkError(message) => {
// Handle network failure
}
DecodeError(message) => {
// Handle a response contract mismatch
}
}
}
invoke_typed_action_key(
create_user_action,
CreateUserRequest::{ name: "John", email: "john@example.com" },
handle_create_user,
)let create_user_action : TypedActionKey[CreateUserRequest, CreateUserResponse] =
@types.action_create_user_typed()
let create_user = create_typed_action_invoker_key(create_user_action)
// Later...
create_user(
CreateUserRequest::{ name: "Jane", email: "jane@example.com" },
fn(response) {
match response {
Success(data) => save_user_id(data.id)
_ => ()
}
},
)let create_user_action : TypedActionKey[CreateUserRequest, CreateUserResponse] =
@types.action_create_user_typed()
submit_form_as_action(
form_element,
ActionFormConfig::from_key(create_user_action)
.with_success(fn(data) { save_user_id(data.id) })
)let state = @signal.signal(ActionState::idle())
state.set(ActionState::pending(message="Saving"))
invoke_typed_action_key(@types.action_create_user_typed(), payload, fn(response) {
match response {
Success(_) => state.set(ActionState::success(message="Saved"))
Redirect(url) => state.set(ActionState::redirect(url))
Error(status, message) => state.set(ActionState::error(status~, message~))
NetworkError(message) => state.set(ActionState::error(status=0, message~))
DecodeError(message) => state.set(ActionState::error(status=0, message~))
}
})| Result | Description |
|---|---|
| Success(data) | Operation succeeded, return JSON data |
| Redirect(url) | Operation succeeded, redirect client |
| ClientError(status, msg) | Client error (4xx) |
| ServerError(msg) | Server error (logged, not exposed) |
// Full CSRF protection
@middleware.csrf_for_origin("https://example.com")
// Origin-only validation
@middleware.validate_origin_middleware(["https://example.com"])
// Fetch Metadata validation
@middleware.fetch_metadata()
// Require JSON Content-Type
@middleware.require_json_content_type()CsrfConfig::default()
.with_origins(["https://example.com", "https://api.example.com"])
.with_fetch_metadata(true)
.require_json()
.with_error_message("Access Denied")ActionDef::from_key(@types.action_my_action(), handler)
.with_require_json(true) // Require application/json
.with_middleware(auth_middleware) // Add custom middlewareActionRegistry::new(allowed_origins=["https://example.com"])
.with_base_path("/api/action") // Custom base path
.register_key(@types.action_my_action(), handler)pub(open) trait ActionKey {
fn id(Self) -> String
fn base_path(Self) -> String
}pub struct ActionDef {
// private fields
}pub struct ActionFormConfig[Res] {
// private fields
}fn[Res] ActionFormConfig::with_error(self : ActionFormConfig[Res], on_error : (Int, String) -> Unit) -> ActionFormConfig[Res]fn[Res] ActionFormConfig::with_method(self : ActionFormConfig[Res], form_method : String) -> ActionFormConfig[Res]fn[Res] ActionFormConfig::with_redirect(self : ActionFormConfig[Res], on_redirect : (String) -> Bool) -> ActionFormConfig[Res]fn[Res] ActionFormConfig::with_success(self : ActionFormConfig[Res], on_success : (Res) -> Unit) -> ActionFormConfig[Res]pub struct ActionRegistry {
// private fields
}fn[K : ActionKey] ActionRegistry::register_key(self : ActionRegistry, key : K, handler : ActionHandler) -> ActionRegistrypub struct ActionResult {
// private fields
}pub struct TypedActionKey[Req, Res] {
id : String
base_path : String
_request : Req?
_response : Res?
}impl ActionKey for TypedActionKey[Req, Res]pub enum TypedActionResponse[Res] {
Success(Res)
Redirect(String)
Error(Int, String)
NetworkError(String)
DecodeError(String)
}fn[Req : ToJson, Res : FromJson] create_typed_action_invoker_key(key : TypedActionKey[Req, Res], options? : ActionOptions) -> ((Req, (TypedActionResponse[Res]) -> Unit) -> Unit)fn[Req : ToJson, Res : FromJson] invoke_typed_action_key(key : TypedActionKey[Req, Res], payload : Req, callback : (TypedActionResponse[Res]) -> Unit, options? : ActionOptions) -> Unitfn[Res : FromJson] submit_form_as_action(form : HTMLFormElement, config : ActionFormConfig[Res]) -> UnitInstall
Download zipThin Mars wrapper for file-based routing, SSR, and asset loading