Sign in

    #Server Action

    Server Actions are secure server-side functions that can be called from the client. They automatically include CSRF protection based on modern browser security features.

    #Security Design

    Based on jxck's CSRF article, the security model prioritizes:

    1. Origin Header Validation (Primary Defense)
    2. Fetch Metadata Validation (Sec-Fetch-Site, Sec-Fetch-Mode)
    3. Content-Type Validation (for JSON APIs)
    4. SameSite Cookie (Secondary Effect)

    CSRF tokens are intentionally NOT included as they add complexity without significant benefit when proper Origin validation is in place.

    #Server-Side Usage

    #1. Define Actions

    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")
    })

    #2. Register Actions

    // `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)
    )

    #3. Mount to Hono App

    let app = @hono.Hono::new()
    let app = register_actions(app, registry)

    This registers endpoints at:
    • POST /_action/create-user
    • POST /_action/delete-user

    #Client-Side Usage

    #Basic Invocation

    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,
    )

    #Create Reusable Invoker

    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)
    _ => ()
    }
    },
    )

    #Form Integration

    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) })
    )

    #Pending / Success / Error State

    Use ActionState when the UI needs standard action affordances without hand-rolling every state transition:

    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~))
    }
    })

    #ActionResult Types

    ResultDescription
    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)

    ActionResult::validation_error(message) is a convenience helper for 422 validation failures.

    #Security Middleware

    The CSRF middleware can also be used standalone:

    // 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()

    #Configuration Options

    #CsrfConfig

    CsrfConfig::default()
    .with_origins(["https://example.com", "https://api.example.com"])
    .with_fetch_metadata(true)
    .require_json()
    .with_error_message("Access Denied")

    #ActionKey / ActionDef

    ActionDef::from_key(@types.action_my_action(), handler)
    .with_require_json(true) // Require application/json
    .with_middleware(auth_middleware) // Add custom middleware

    Action IDs are derived from generated action key factories. Hand-written action ID strings are not part of the public action definition API.

    #ActionRegistry

    ActionRegistry::new(allowed_origins=["https://example.com"])
    .with_base_path("/api/action") // Custom base path
    .register_key(@types.action_my_action(), handler)

    App

    using @mizchi/mars { type Server as App }

    Mars App type alias

    Ctx

    using @mizchi/mars { type Context as Ctx }

    Mars Context type alias

    ActionKey

    pub(open) trait ActionKey {
    fn id(Self) -> String
    fn base_path(Self) -> String
    }

    Action key implemented by generated marker types.

    ActionContext

    pub struct ActionContext {
    ctx :
    Context

    body : String
    }

    Action handler context - provides request info and utilities

    ActionContext::decode_json

    Decode this action request body as typed JSON.

    ActionContext::from_ctx

    fn ActionContext::from_ctx(ctx :
    Context
    , body : String) -> ActionContext

    Create ActionContext from Mars context

    ActionContext::get_header

    fn ActionContext::get_header(self : ActionContext, name : String) -> String?

    Get header value (case-insensitive)

    ActionContext::get_query

    fn ActionContext::get_query(self : ActionContext, name : String) -> String?

    Get query parameter

    ActionContext::get_state

    fn ActionContext::get_state(self : ActionContext, key : String) -> String?

    Get state value

    ActionContext::set_state

    fn ActionContext::set_state(self : ActionContext, key : String, value : String) -> Unit

    Set state value

    ActionDef

    pub struct ActionDef {
    // private fields
    }

    Server Action definition

    ActionDef::from_key

    fn[K : ActionKey] ActionDef::from_key(key : K, handler : ActionHandler) -> ActionDef

    Create action definition from a generated action key.

    ActionDef::with_middleware

    Add middleware to action

    ActionDef::with_middlewares

    Add multiple middlewares to action

    ActionDef::with_require_json

    fn ActionDef::with_require_json(self : ActionDef, require : Bool) -> ActionDef

    Set JSON requirement

    ActionFormConfig

    pub struct ActionFormConfig[Res] {
    // private fields
    }

    Action form configuration

    ActionFormConfig::from_key

    fn[Req, Res] ActionFormConfig::from_key(key : TypedActionKey[Req, Res]) -> ActionFormConfig[Res]

    Create form config from a generated action key

    ActionFormConfig::with_error

    fn[Res] ActionFormConfig::with_error(self : ActionFormConfig[Res], on_error : (Int, String) -> Unit) -> ActionFormConfig[Res]

    Set error callback for action form submission.

    ActionFormConfig::with_method

    fn[Res] ActionFormConfig::with_method(self : ActionFormConfig[Res], form_method : String) -> ActionFormConfig[Res]

    Set form method for action form submission.

    ActionFormConfig::with_redirect

    fn[Res] ActionFormConfig::with_redirect(self : ActionFormConfig[Res], on_redirect : (String) -> Bool) -> ActionFormConfig[Res]

    Set redirect callback for action form submission.

    ActionFormConfig::with_success

    fn[Res] ActionFormConfig::with_success(self : ActionFormConfig[Res], on_success : (Res) -> Unit) -> ActionFormConfig[Res]

    Set success callback for action form submission.

    ActionHandler

    pub(all) struct ActionHandler(async (ActionContext) -> ActionResult)

    Action handler type - async function that receives ActionContext and returns ActionResult

    ActionOptions

    pub struct ActionOptions {
    headers : Array[(String, String)]
    timeout : Int?
    signal :
    AbortSignal
    ?
    }

    Action invocation options

    ActionOptions::default

    fn ActionOptions::default() -> ActionOptions

    Default action options

    ActionPhase

    pub enum ActionPhase {
    Idle
    Pending
    Succeeded
    Failed
    Redirecting
    } derive(Eq,
    Debug
    )

    UI-facing action phase for common form/action states

    ActionRegistry

    pub struct ActionRegistry {
    // private fields
    }

    Registry for server actions

    ActionRegistry::action_ids

    fn ActionRegistry::action_ids(self : ActionRegistry) -> Array[String]

    Get all action IDs

    ActionRegistry::new

    fn ActionRegistry::new(allowed_origins? : Array[String]) -> ActionRegistry

    Create new action registry

    ActionRegistry::register

    fn ActionRegistry::register(self : ActionRegistry, action : ActionDef) -> ActionRegistry

    Register an action

    ActionRegistry::register_all

    fn ActionRegistry::register_all(self : ActionRegistry, defs : Array[ActionDef]) -> ActionRegistry

    Register multiple actions

    ActionRegistry::register_key

    fn[K : ActionKey] ActionRegistry::register_key(self : ActionRegistry, key : K, handler : ActionHandler) -> ActionRegistry

    Register an action from a generated action key and handler

    ActionRegistry::with_base_path

    fn ActionRegistry::with_base_path(self : ActionRegistry, base_path : String) -> ActionRegistry

    Set base path for action endpoints

    ActionResult

    pub struct ActionResult {
    // private fields
    }

    Action result returned by server action handlers.

    ActionResult::bad_request

    fn ActionResult::bad_request(message : String) -> ActionResult

    Create bad request error

    ActionResult::forbidden

    fn ActionResult::forbidden(message? : String) -> ActionResult

    Create forbidden error

    ActionResult::http_redirect

    fn ActionResult::http_redirect(url : String) -> ActionResult

    Create HTTP redirect result (returns 302 with Location header) Use this for non-JavaScript form submissions (progressive enhancement)

    ActionResult::json

    fn[T : ToJson] ActionResult::json(data : T) -> ActionResult

    Create success result from a typed JSON-serializable payload.

    ActionResult::not_found

    fn ActionResult::not_found(message? : String) -> ActionResult

    Create not found error

    ActionResult::redirect

    fn ActionResult::redirect(url : String) -> ActionResult

    Create redirect result (returns JSON for JavaScript clients)

    ActionResult::server_error

    fn ActionResult::server_error(message? : String) -> ActionResult

    Create server error

    ActionResult::unauthorized

    fn ActionResult::unauthorized(message? : String) -> ActionResult

    Create unauthorized error

    ActionResult::validation_error

    fn ActionResult::validation_error(message : String) -> ActionResult

    Create validation error

    ActionState

    pub struct ActionState {
    phase : ActionPhase
    status : Int?
    message : String?
    redirect : String?
    }

    UI-facing action state for pending/error/success controls

    ActionState::error

    fn ActionState::error(status~ : Int, message~ : String) -> ActionState

    Failed action state

    ActionState::idle

    fn ActionState::idle() -> ActionState

    Initial idle action state

    ActionState::pending

    fn ActionState::pending(message? : String) -> ActionState

    Pending action state

    ActionState::redirect

    fn ActionState::redirect(url : String) -> ActionState

    Redirecting action state

    ActionState::success

    fn ActionState::success(message? : String) -> ActionState

    Successful action state

    TypedActionKey

    pub struct TypedActionKey[Req, Res] {
    id : String
    base_path : String
    _request : Req?
    _response : Res?
    }

    Phantom-typed action key for generated request/response contracts.
    impl ActionKey for TypedActionKey[Req, Res]

    TypedActionKey::base_path

    fn[Req, Res] TypedActionKey::base_path(self : TypedActionKey[Req, Res]) -> String

    TypedActionKey::from_key

    fn[Req, Res, K : ActionKey] TypedActionKey::from_key(key : K) -> TypedActionKey[Req, Res]

    TypedActionKey::id

    fn[Req, Res] TypedActionKey::id(self : TypedActionKey[Req, Res]) -> String

    TypedActionResponse

    pub enum TypedActionResponse[Res] {
    Success(Res)
    Redirect(String)
    Error(Int, String)
    NetworkError(String)
    DecodeError(String)
    }

    Typed action client response.

    action_id

    fn[K : ActionKey] action_id(key : K) -> String

    Get the stable action identifier from a generated action key.

    action_url

    fn[K : ActionKey] action_url(key : K) -> String

    Get the full endpoint URL from a generated action key.

    create_typed_action_invoker_key

    fn[Req : ToJson, Res :
    FromJson
    ] create_typed_action_invoker_key(key : TypedActionKey[Req, Res], options? : ActionOptions) -> ((Req, (TypedActionResponse[Res]) -> Unit) -> Unit)

    Create an action invoker whose payload parameter is tied to a typed key.

    decode_action_body

    fn[T :
    FromJson
    ] decode_action_body(body : String) -> T?

    Decode an action request body as typed JSON.

    invoke_typed_action_key

    fn[Req : ToJson, Res :
    FromJson
    ] invoke_typed_action_key(key : TypedActionKey[Req, Res], payload : Req, callback : (TypedActionResponse[Res]) -> Unit, options? : ActionOptions) -> Unit

    Invoke a typed server action using its generated request payload type.

    register_actions

    fn register_actions(app :
    Server
    , registry : ActionRegistry) -> Unit

    Register all actions from registry to Mars app

    submit_form_as_action

    Handle form submission as server action

    typed_action_url

    fn[Req, Res] typed_action_url(key : TypedActionKey[Req, Res]) -> String

    Powered by MoonBit

    Site sourceReport issuePackagesBuild queueSkillsStatistics

    © 2026 mooncakes.io