Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Tutorial 52: Typed Server Functions

#[server_function] lets one concrete async Rust signature describe both the native server implementation and its Wasm caller. The transport is explicit: the macro also creates a <function>_rpc_router() that you mount in the server. It does not discover routes through runtime reflection.

use rullst::{Router, server_function};
use serde::{Deserialize, Serialize};

#[derive(Debug, Deserialize, Serialize)]
pub struct SumResponse {
    pub value: u32,
}

#[server_function(path = "/api/rpc/math/add")]
pub async fn add(left: u32, right: u32) -> rullst::rpc::RpcResult<SumResponse> {
    Ok(SumResponse {
        value: left.saturating_add(right),
    })
}

pub fn rpc_routes() -> Router {
    add_rpc_router()
}

fn main() {
    let _router = rpc_routes();
}

When this function is compiled for the native server, its written body runs. When compiled for wasm32, calling add(20, 22).await serializes (20, 22) and returns the decoded RpcResult<SumResponse>. Transport failures are machine-readable RpcFailure values; they never become a fabricated default application value.

Mount the generated route inside server policy

Merge the generated router before applying the standard security baseline and your domain layers:

use rullst::{Router, server_function};
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize)]
pub struct SumResponse { pub value: u32 }
#[server_function(path = "/api/rpc/math/add")]
pub async fn add(left: u32, right: u32) -> rullst::rpc::RpcResult<SumResponse> {
    Ok(SumResponse { value: left.saturating_add(right) })
}
fn secured_transport() -> Result<axum::Router, rullst::SecurityBaselineError> {
    let app = Router::new()
        .merge_axum(add_rpc_router().into_axum())
        .into_axum();

    rullst::apply_security_baseline(
        app,
        rullst::SecurityConfig::default(),
        rullst::config::Environment::Production,
    )
}

fn main() -> Result<(), rullst::SecurityBaselineError> {
    let _app = secured_transport()?;
    Ok(())
}

The production baseline verifies the double-submit CSRF cookie/header pair. The Wasm caller reads the bounded rullst_csrf cookie and forwards it as X-CSRF-Token. The application must still add session/authentication, trusted tenant resolution, object/role authorization and rate limiting in the order documented by ProductionPreset. Never accept role, owner or tenant authority from a function argument.

Failure codes

Application failures use the same lowercase dotted-code grammar as the shared client contract:

#![allow(unused)]
fn main() {
fn capacity_failure() -> Result<rullst::rpc::RpcFailure, rullst::client_contract::ClientContractError> {
    rullst::rpc::RpcFailure::application("course.capacity_reached", false)
}

let failure = capacity_failure()?;
assert_eq!(failure.code(), "course.capacity_reached");
Ok::<(), rullst::client_contract::ClientContractError>(())
}

Do not place provider bodies, database errors, PII, tokens or debug text in a failure code. Log sensitive diagnostics only through an approved server-side telemetry policy.

Exact v12 limits

  • zero to 16 simple identifier parameters;
  • owned parameter types and one owned output type implementing the needed Serde traits;
  • a concrete async free function with no generics, receiver, unsafe, extern ABI or variadic arguments;
  • rullst::rpc::RpcResult<T> as the return type;
  • an optional same-origin path below /api/rpc/, using at most 128 ASCII bytes;
  • 256 KiB encoded request and response policy;
  • JSON POST transport with a versioned envelope and request-ID correlation.

These bounds do not make a mutation exactly once. Put a stable idempotency key in the domain payload and enforce it transactionally on the server when replay would be harmful. Actual browser-engine compatibility, network availability and deployed identity policy must be tested by the application.