// In your app crate, alongside your UI code: use server::{server, ServerError}; #[server] async fn list_todos(user_id: u64) -> Result<Vec<Todo>, ServerError> { let db = server::use_state::<Arc<Db>>() .ok_or_else(|| ServerError::failed("Db not installed"))?; db.query("SELECT * FROM todos WHERE user_id = $1", &[&user_id]).await } // In the very same crate, in your UI component: let todos = list_todos(current_user.id).await?;
// Server build: --features server async fn add(a: i32, b: i32) -> Result<i32, ServerError> { Ok(a + b) // original body } // Plus an inventory::submit! that registers a handler: // POST /_srv/add → decode args → call add(a, b) → encode result
// Client build: default features async fn add(a: i32, b: i32) -> Result<i32, ServerError> { server::__private::call::<(i32, i32), _>("add", &(a, b)).await }
# single call POST /_srv/<path> Content-Type: application/json [arg0, arg1, ...] → {"Ok": T} | {"Err": E} # batched calls (microtask-coalesced) POST /_srv/_batch [{"path": "add", "args": [2, 3]}, {"path": "v1/ping", "args": null}] → [{"Ok": 5}, {"Ok": "pong"}]
my-app/ ├── shared/ # types + #[server] fns + cfg-gated server state ├── server/ # bin, depends on shared with features=["server"] └── client/ # one or more clients (web wasm, native, mobile); # depend on shared with default features
// shared/src/server_fns.rs // ❌ leaks: `use diesel::*` at module scope compiles in // both modes. If diesel isn't in the client's dep graph, this errors. use diesel::prelude::*; // ✅ clean: cfg-gated import, only compiled with the server half #[cfg(feature = "server")] use diesel::prelude::*;
// At server startup: server::install_state(Arc::new(Db::connect().await)); // Inside any server fn body: #[server] async fn list_todos(user_id: u64) -> Result<Vec<Todo>, ServerError> { let db = server::use_state::<Arc<Db>>() .ok_or_else(|| ServerError::failed("Db not installed"))?; db.query(...).await }
#[server] async fn whoami() -> Result<String, ServerError> { let auth = server::use_request_header("authorization") .ok_or_else(|| ServerError::failed("missing Authorization"))?; Ok(format!("authenticated as: {auth}")) }
// Three calls in the same tick: let (user, todos, projects) = tokio::join!( get_user(uid), list_todos(uid), list_projects(), ); // → one POST /_srv/_batch on the wire, not three.
let user_id = signal(1u64); let user = resource(user_id, |id, resource_cancel| async move { server::with_cancel(resource_cancel, get_user(id)).await }); // `user_id.set(2)` cancels: // 1. the resource's prior fetch (ResourceCancel) // 2. the in-flight HTTP request (net::CancelToken) // 3. the actual network read (reqwest drops / browser aborts / iOS // task.cancel / Android conn.disconnect)
let todos: Signal<Vec<Todo>> = signal!(Vec::new()); // load on mount, refresh on dep change let refresh = async_reducer( todos, |_| async { list_todos().await }, |list, new_list| *list = new_list, ); // mutation that folds response straight into local state let create = async_reducer( todos, |input| async move { create_todo(input).await }, |list, new_todo| list.push(new_todo), );
# Cargo.toml [package.metadata.idealyst.app] targets = ["web"] server_bin = "server" # opt the project into the full-stack flow # one command — builds wasm, runs the server bin, watches src/ for changes: idealyst dev --web --local my-app