How to Use the #[procedure] Macro for Async Server Functions in Tokio Topcoat
The #[procedure] macro transforms async Rust functions into server-side endpoints that client-side runtime expressions can invoke over the network.
The #[procedure] macro in the tokio-rs/topcoat repository bridges the gap between server-side Rust logic and browser-based interactions. By annotating an async function with this macro, you expose it as a callable route on the application's Router, enabling seamless client-server communication through Topcoat's reactive view layer.
Understanding the #[procedure] Macro
In crates/topcoat-runtime/src/procedure.rs, the #[procedure] macro generates the boilerplate necessary to register async functions as HTTP endpoints. When applied to a function, it implements the Procedure trait and creates the serialization glue required to translate between JavaScript values and Rust types.
The macro integrates with Topcoat's router system defined in crates/topcoat-router/src/router.rs. Procedures become accessible to the client-side expr! runtime, which handles the network request and deserialization automatically.
Defining Server Procedures
Basic Function Signatures
Procedure functions must be async and return a Result<T, E> where the Ok variant contains types compatible with Topcoat's shared vocabulary. According to the documentation in crates/topcoat-runtime/macro/docs/procedure.md, supported types include primitives, strings, and collections that serialize to JavaScript.
use topcoat::{Result, runtime::procedure};
#[procedure]
async fn double(value: f64) -> Result<f64> {
Ok(value * 2.0)
}
The Request Context (cx) Parameter
You can add a special cx parameter of type &Cx to access request-scoped data. This argument is injected by the server and excluded from the client-side signature. The Cx type provides access to authentication state, session data, and application context.
use topcoat::{Result, context::Cx, runtime::procedure};
#[procedure]
async fn search(cx: &Cx, query: String) -> Result<String> {
// Access database or auth info through cx
Ok(query)
}
Registering Procedures with the Router
Automatic Discovery
The router discovers procedures automatically when you call .discover() on the Router builder. This method walks the binary to find all linked procedures marked with #[procedure] and mounts them as routes.
Manual Registration
For explicit control, use RouterBuilderProcedureExt::procedure to mount individual functions. In crates/topcoat-router/src/router.rs, this extension trait adds the .procedure() method to the builder pattern.
use topcoat::{Result, router::Router, runtime::{procedure, RouterBuilderProcedureExt}};
#[procedure]
async fn greet(name: String) -> Result<String> {
Ok(format!("Hello, {name}!"))
}
let router = Router::builder()
.procedure(greet)
.build();
Calling Procedures from the Client
Runtime Expressions (expr!)
Client-side code invokes procedures inside runtime expressions defined by the expr! macro (documented in crates/topcoat-runtime/macro/docs/expr.md). These expressions execute in an async context, allowing you to .await the procedure call.
use topcoat::{Result, view::*, runtime::procedure};
#[component]
async fn counter() -> Result {
view! {
signal count = 1.0;
<button @click=$(async |_e| {
let doubled = double(count.get()).await;
count.set(doubled);
})>
"double it"
</button>
$(count.get())
}
}
Handling Async Calls
Because procedure calls traverse the network, they must occur within an async block. The view! macro (documented in crates/topcoat-view/macro/docs/view.md) handles the reactive updates when the awaited call resolves.
Error Handling and Security
Result Types and Error Propagation
When a procedure returns Ok(v), the awaiting expression resolves to v. If it returns Err(e), the server responds with an error status and the client-side expression aborts without returning a value. To expose errors to the UI, return a Result<T, E> as the success type instead of propagating errors through the ? operator.
Security Considerations with cx
Never trust procedure arguments directly, as clients can spoof them. Always validate inputs and use the cx parameter to verify authentication or authorization before performing privileged operations. The request context provides the trusted source of identity and session state.
Summary
- The
#[procedure]macro intokio-rs/topcoatexposes async Rust functions as HTTP endpoints callable from browser-side code. - Functions must return
Result<T, E>with serializable types, and may accept an optionalcx: &Cxparameter for request context. - Register procedures via automatic
.discover()or manual.procedure()on theRouterbuilder. - Call procedures from within
expr!runtime expressions using standard.awaitsyntax inside async blocks. - Handle errors by returning them as data rather than using
?, and always verify security through thecxcontext rather than trusting client arguments.
Frequently Asked Questions
What types can I use as arguments to a #[procedure] function?
Arguments and return values must belong to Topcoat's shared vocabulary that serializes to JavaScript. This includes primitives like f64, String, and standard collections. Complex types must implement the necessary serialization traits to cross the JavaScript/WebAssembly boundary.
How do I access database connections inside a procedure?
Use the cx: &Cx parameter to access application context. Inside the function, call methods on cx to retrieve request-scoped resources like database pools or connection handles. This parameter is automatically injected by the server and is not part of the client-side call signature.
What happens if the procedure returns an error?
If the procedure returns Err(e), the server sends an error response, and the client-side await aborts without producing a value. The expression stops executing. To handle errors gracefully in the UI, return a Result<T, E> as the Ok variant and pattern match on the client side.
Can I use #[procedure] with synchronous functions?
No. The #[procedure] macro requires async functions because it handles concurrent request processing and non-blocking I/O. If you have synchronous logic, wrap it in an async block or function when defining the procedure endpoint.
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 →