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

    let create_user_handler = ActionHandler(async fn(ctx) {
    // Parse request body
    let body = ctx.body
    // ... validate and process

    // Return success
    ActionResult::ok(@js.any({ "id": 123, "name": "John" }))
    })

    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

    let registry = ActionRegistry::new(
    allowed_origins=["https://example.com"]
    )
    .register(ActionDef::new("create-user", create_user_handler))
    .register(
    ActionDef::new("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 response = invoke_action(
    "/_action/create-user",
    @js.any({ "name": "John", "email": "john@example.com" })
    )

    match response {
    Success(data) => {
    // Handle success
    let id = data["id"]
    }
    Redirect(url) => {
    // Handle redirect (usually automatic)
    }
    Error(status, message) => {
    // Handle error
    }
    NetworkError(message) => {
    // Handle network failure
    }
    }

    #Create Reusable Invoker

    let create_user = create_action_invoker("/_action/create-user")

    // Later...
    let response = create_user(@js.any({ "name": "Jane" }))

    #Form Integration

    submit_form_as_action(
    form_element,
    ActionFormConfig::new("/_action/create-user")
    )

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

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

    #ActionDef

    ActionDef::new("my-action", handler)
    .with_require_json(true) // Require application/json
    .with_middleware(auth_middleware) // Add custom middleware

    #ActionRegistry

    ActionRegistry::new(allowed_origins=["https://example.com"])
    .with_base_path("/api/action") // Custom base path
    .register(action1)
    .register_all([action2, action3])

    App

    using @mizchi/mars { type Server as App }

    Mars App type alias

    Ctx

    using @mizchi/mars { type Context as Ctx }

    Mars Context type alias

    ActionContext

    pub struct ActionContext {
    ctx :
    Context

    body : String
    }

    Action handler context - provides request info and utilities

    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 {
    id : String
    handler : ActionHandler
    middlewares : Array[
    Middleware
    ]
    require_json : Bool
    }

    Server Action definition

    ActionDef::new

    fn ActionDef::new(id : String, handler : ActionHandler) -> ActionDef

    Create action definition with default settings

    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 {
    action : String
    form_method : String
    on_success : (
    Any
    ) -> Unit?
    on_error : (Int, String) -> Unit?
    on_redirect : (String) -> Bool?
    }

    Action form configuration

    ActionFormConfig::from_ref

    Create form config from a type-safe ActionRef

    ActionFormConfig::new

    fn ActionFormConfig::new(action : String) -> ActionFormConfig

    Default form config

    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 :
    Any
    ?
    }

    Action invocation options

    ActionOptions::default

    fn ActionOptions::default() -> ActionOptions

    Default action options

    ActionRef

    pub(all) struct ActionRef {
    id : String
    base_path : String
    }

    Type-safe action reference. Factory functions can be auto-generated by sol generate from action definitions.

    Example:
    // Define action ref (can be auto-generated)
    pub fn create_user() -> ActionRef {
    { id: "create-user", base_path: "/_action" }
    }

    // Register with handler
    registry.register_ref(@actions.create_user(), handler=handler)

    // Client-side invocation
    @action.invoke_action(@actions.create_user().url(), payload, callback)

    ActionRef::to_def

    fn ActionRef::to_def(self : ActionRef, handler : ActionHandler) -> ActionDef

    Create an ActionDef from this ref and a handler

    ActionRef::url

    fn ActionRef::url(self : ActionRef) -> String

    Get the full URL for this action

    ActionRegistry

    pub struct ActionRegistry {
    actions : Map[String, ActionDef]
    base_path : String
    allowed_origins : Array[String]
    }

    Registry for server actions

    ActionRegistry::action_ids

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

    Get all action IDs

    ActionRegistry::action_url

    fn ActionRegistry::action_url(self : ActionRegistry, id : String) -> String

    Get action endpoint URL

    ActionRegistry::get

    fn ActionRegistry::get(self : ActionRegistry, id : String) -> ActionDef?

    Get action by ID

    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_ref

    fn ActionRegistry::register_ref(self : ActionRegistry, aref : ActionRef, handler : ActionHandler) -> ActionRegistry

    Register an action from a type-safe ActionRef and handler

    ActionRegistry::with_base_path

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

    Set base path for action endpoints

    ActionResponse

    pub enum ActionResponse {
    Success(
    Any
    )
    Redirect(String)
    Error(Int, String)
    NetworkError(String)
    }

    Action client response

    ActionResponse::data

    Get data from successful response

    ActionResponse::error_message

    fn ActionResponse::error_message(self : ActionResponse) -> String?

    Get error message

    ActionResponse::is_error

    fn ActionResponse::is_error(self : ActionResponse) -> Bool

    Check if response is an error

    ActionResponse::is_redirect

    fn ActionResponse::is_redirect(self : ActionResponse) -> Bool

    Check if response is a redirect

    ActionResponse::is_success

    fn ActionResponse::is_success(self : ActionResponse) -> Bool

    Check if response is successful

    ActionResponse::redirect_url

    fn ActionResponse::redirect_url(self : ActionResponse) -> String?

    Get redirect URL

    ActionResult

    pub enum ActionResult {
    Success(
    Any
    )
    Redirect(String)
    HttpRedirect(String)
    ClientError(Int, String)
    ServerError(String)
    }

    Action result - either success with data or error

    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::not_found

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

    Create not found error

    ActionResult::ok

    Create success result

    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

    create_action_invoker

    fn create_action_invoker(url : String, options? : ActionOptions) -> ((
    Any
    , (ActionResponse) -> Unit) -> Unit)

    Create an action invoker for a specific endpoint Returns a function that takes payload and callback

    create_action_invoker_ref

    fn create_action_invoker_ref(aref : ActionRef, options? : ActionOptions) -> ((
    Any
    , (ActionResponse) -> Unit) -> Unit)

    Create an action invoker from a type-safe ActionRef

    invoke_action

    fn invoke_action(url : String, payload :
    Any
    , callback : (ActionResponse) -> Unit, options? : ActionOptions) -> Unit

    Invoke a server action with callback
    • url: The action endpoint URL (e.g., "/_action/create-user")
    • payload: The JSON payload to send
    • callback: Function to call with the response
    • options: Optional configuration

    invoke_action_ref

    fn invoke_action_ref(aref : ActionRef, payload :
    Any
    , callback : (ActionResponse) -> Unit, options? : ActionOptions) -> Unit

    Invoke a server action using a type-safe ActionRef

    register_actions

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

    Register all actions from registry to Mars app

    submit_form_as_action

    fn submit_form_as_action(form :
    Any
    , config : ActionFormConfig) -> Unit

    Handle form submission as server action

    Powered by MoonBit

    Site sourceReport issuePackagesBuild queueSkillsStatistics

    © 2026 mooncakes.io