How Dioxus Server Functions Work: Full-Stack Rust Development Explained
Dioxus Server Functions use a #[server] procedural macro to compile a single async function into both an Axum HTTP handler that runs on the server and a client-side stub that serializes calls over fetch, enabling type-safe full-stack development with automatic route registration.
Dioxus Server Functions bridge the gap between frontend and backend code in the DioxusLabs/dioxus ecosystem, allowing developers to write server logic once and invoke it from the browser as if it were a local async function. By leveraging procedural macros and compile-time code generation, the framework eliminates the need for manual API endpoint creation while maintaining type safety across the network boundary. This architecture centers on the #[server] attribute, which transforms annotated functions into a coordinated server handler and client SDK.
How the #[server] Macro Expands Your Code
When you attach the #[server] attribute to an async function, the procedural macro defined in packages/fullstack-macro/src/lib.rs performs a compile-time transformation. It expands your single function definition into two distinct implementations: a server-side HTTP handler and a client-side invocation stub.
Server-Side Handler Generation
On the server, the macro generates an axum::MethodRouter that acts as the HTTP endpoint. According to the source in packages/fullstack-server/src/serverfn.rs, the generated code creates a ServerFunction struct instance that captures the route path, HTTP method, and handler closure.
The handler performs three critical operations:
- Context Extraction: It builds a
FullstackContextfrom the incoming request parts usingFullstackContext::new(parts), allowing access to cookies, headers, and other server-specific resources. - Scoped Execution: It runs your original function body inside
FullstackContext::scope, which provides a structured way to extract typed arguments usingFullstackContext::extract. - Response Construction: It serializes the return value to JSON and merges any response headers set during execution, with special handling for HTML form POSTs that may trigger a
302 Foundredirect to theReferer.
The handler runs inside a Tokio LocalPool specifically to support non-Send futures, meaning you can use thread-local state like use_signal inside server functions without requiring Send bounds across the entire server.
Client-Side Stub Generation
On the client, the macro generates a stub function with the identical signature to your original. This stub:
- Serializes arguments to JSON using
serde_json - Issues an HTTP
fetchrequest to the generated endpoint (mounted under/__dioxus_server/<path>by default) - Deserializes the JSON response and returns it as a
Futurethat resolves toResult<T, ServerFnError>
This creates the illusion of a local function call while performing network communication under the hood.
The ServerFunction Registry and Route Mounting
The macro automatically registers every server function using the inventory crate. As shown in packages/fullstack-server/src/serverfn.rs, the ServerFunction struct provides a constant constructor and a collection mechanism:
pub struct ServerFunction {
path: &'static str,
method: Method,
handler: fn() -> MethodRouter<FullstackState>,
}
impl ServerFunction {
pub const fn new(method: Method, path: &'static str, handler: fn() -> MethodRouter<FullstackState>) -> Self { … }
pub fn collect() -> Vec<&'static ServerFunction> {
inventory::iter::<ServerFunction>().collect()
}
}
When the fullstack server starts (implemented in packages/fullstack-server/src/server.rs), it calls ServerFunction::collect() to retrieve all registered functions and mounts each MethodRouter onto the main axum::Router. This eliminates manual route registration—simply defining a server function makes it available as an HTTP endpoint.
Request Handling Inside FullstackContext
During request processing, the generated handler utilizes FullstackContext to manage the execution environment. The context wraps the request parts and provides a scoped execution model where your function code runs. When you call FullstackContext::extract, the system deserializes the request body (typically JSON) into your function's argument types.
The handler also manages response modifications. If your server function sets headers or returns specific data, these merge into the final axum::Response. For traditional server-rendered form submissions, the system supports automatic redirects back to the referring page, maintaining compatibility with classic web form behavior while still providing the modern RPC-style API.
Why Local Pool Execution Matters
A distinctive feature of Dioxus Server Functions is their use of a local task pool via LocalPool. This design choice allows server functions to contain !Send futures—futures that are not thread-safe or contain thread-local data. In practice, this means you can use Dioxus signals and other non-Send types directly within server function bodies without encountering compilation errors about Send bounds, which would typically be required for standard Axum handlers that must be Send.
Practical Implementation Example
Consider a simple greeting function defined using the #[server] macro:
#[server]
async fn greet(name: String) -> String {
format!("Hello, {name}!")
}
The macro expansion generates the server-side registration:
pub const __ENDPOINT_PATH: &str = "/greet";
pub fn __router() -> MethodRouter<FullstackState> {
ServerFunction::make_handler(Method::POST, |state, req| {
Box::pin(async move {
let (name,) = FullstackContext::extract::<(String,)>(state).await?;
let result = greet(name).await;
Response::builder()
.status(StatusCode::OK)
.header("content-type", "application/json")
.body(Body::from(serde_json::to_string(&result)?))
.unwrap()
})
})
}
inventory::submit! {
ServerFunction::new(Method::POST, __ENDPOINT_PATH, __router)
}
And the client-side stub:
pub async fn greet(name: String) -> Result<String, ServerFnError> {
let payload = serde_json::to_string(&(name,))?;
let resp = dioxus_fullstack::fetch(__ENDPOINT_PATH, payload).await?;
let json: String = serde_json::from_slice(&resp.body)?;
Ok(json)
}
You can then call this function directly from a Dioxus component:
#[component]
fn Hello() -> Element {
let greeting = use_resource(|| async move { greet("World".into()).await });
rsx! {
match *greeting.read() {
Some(Ok(msg)) => p { "{msg}" },
Some(Err(e)) => p { "Error: {e}" },
None => p { "Loading…" },
}
}
}
Summary
- Dioxus Server Functions compile a single
#[server]-annotated function into both a server handler and client stub. - The server side uses
axum::MethodRouterandFullstackContextto handle HTTP requests, running user code in aLocalPoolto support!Sendfutures. - The client side generates a type-safe stub that serializes arguments to JSON and deserializes responses over HTTP.
- Automatic registration via
inventory::collect!eliminates manual route setup—functions self-register and mount when the server starts. - FullstackContext provides scoped access to request data, cookies, and headers, with built-in support for form redirects and response header manipulation.
Frequently Asked Questions
What crate implements the #[server] macro in Dioxus?
The #[server] procedural macro is implemented in packages/fullstack-macro/src/lib.rs. This crate handles the compile-time code generation that creates the server-side handler registration and the client-side fetch stub.
How does Dioxus serialize arguments for server functions?
Arguments are serialized to JSON using serde_json on the client side and deserialized within the FullstackContext::extract method on the server. This occurs transparently through the generated stub code, maintaining type safety across the network boundary.
Can server functions access request headers and cookies?
Yes. Server functions run inside a FullstackContext which provides access to the raw request parts. You can extract headers, cookies, query parameters, and other HTTP-specific data using FullstackContext::extract within the scope of the server function.
Why must server functions run in a LocalPool instead of a standard Tokio runtime?
The LocalPool allows server functions to execute !Send futures—futures that contain thread-local data or are not thread-safe. This enables the use of Dioxus signals and other non-Send types directly inside server functions, which would otherwise violate the Send bounds required by standard Axum handlers.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →