Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,11 @@ name = "flat_router"
path = "examples/06-routing/flat_router.rs"
doc-scrape-examples = true

[[example]]
name = "server_functions"
path = "examples/07-fullstack/server_functions.rs"
doc-scrape-examples = true

[[example]]
name = "middleware"
path = "examples/07-fullstack/middleware.rs"
Expand Down
24 changes: 23 additions & 1 deletion examples/07-fullstack/handling_errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
//!
//! Try running requests against the endpoints directly with `curl` or `postman` to see the actual HTTP responses!

use dioxus::fullstack::{Json, StatusCode};
use dioxus::fullstack::{AsStatusCode, Json, StatusCode};
use dioxus::prelude::*;
use serde::{Deserialize, Serialize};

Expand Down Expand Up @@ -164,10 +164,32 @@ enum MyCustomError {
ServerFnError(#[from] ServerFnError),
}

impl AsStatusCode for MyCustomError {
fn as_status_code(&self) -> StatusCode {
match self {
MyCustomError::BadRequest { .. } => StatusCode::BAD_REQUEST,
MyCustomError::NotFound => StatusCode::NOT_FOUND,
MyCustomError::ServerFnError(e) => e.as_status_code(),
}
}
}

/// Returns a custom typed error (serializable) so clients can handle specific cases.
///
/// Our custom error must implement `AsStatusCode` so it can properly set the outgoing HTTP status code.
#[get("/api/typed-error")]
async fn get_throws_typed_error() -> Result<(), MyCustomError> {
Err(MyCustomError::BadRequest {
custom_name: "Invalid input".into(),
})
}

/// Simple POST endpoint used to show a successful server function that returns `StatusCode`.
#[post("/api/data")]
async fn get_throws_serverfn_error() -> Result<(), ServerFnError> {
Err(ServerFnError::ServerError {
message: "Unauthorized access".to_string(),
code: StatusCode::UNAUTHORIZED.as_u16(),
details: None,
})
}
271 changes: 271 additions & 0 deletions examples/07-fullstack/server_functions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
//! This example is a simple showcase of Dioxus Server Functions.
//!
//! The other examples in this folder showcase advanced features of server functions like custom
//! data types, error handling, websockets, and more.
//!
//! This example is meant to just be a simple starting point to show how server functions work.
//!
//! ## Server Functions
//!
//! In Dioxus, Server Functions are `axum` backend endpoints that can be called directly from the client
//! as if you were simply calling a local Rust function. You can do anything with a server function
//! that an Axum handler can do like extracting path, query, headers, and body parameters.
//!
//! ## Server Function Arguments
//!
//! Unlike Axum handlers, the arguments of the server functions have some special magic enabled by
//! the accompanying `#[get]`/`#[post]` attributes. This magic enables you to choose between
//! arguments that are purely serializable (i.e. `String`, `i32`, `Vec<T>`, etc) as the JSON body of
//!
//! the request *or* arguments that implement Axum's `FromRequest` trait. This magic enables simple
//! RPC functions but also complex extractors for things like auth, sessions, cookies, and more.
//!
//! ## Server Function Return Types
//!
//! The return type of the server function is also somewhat magical. Unlike Axum handlers, all server
//! functions must return a `Result` type, giving the client an opportunity to handle errors properly.
//!
//! The `Ok` type can be anything that implements `Serialize + DeserializeOwned` so it can be sent
//! to the client as JSON, or it can be anything that implements `IntoResponse` just like an Axum handler.
//!
//! ## Error Types
//!
//! The `Err` type of the server function return type is also somewhat special. The `Err` type can be:
//! - `anyhow::Error` (the `dioxus_core::Err` type alias) for untyped errors with rich context. Note
//! that these errors will always downcast to `ServerFnError` on the client, losing the original
//! error stack and type.
//! - `ServerFnError` for typed errors with a status code and optional message.
//! - `StatusCode` for returning raw HTTP status codes.
//! - `HttpError` for returning HTTP status codes with custom messages.
//! - Any custom errors that implement `From<ServerFnError>` and are `Serialize`/`Deserialize`
//!
//! The only way to set the HTTP status code of the response is to use one of the above error types,
//! or to implement a custom `IntoResponse` type that sets the status code manually.
//!
//! The `anyhow::Error` type is the best choice for rapid development, but is somewhat limited when
//! handling specific error cases on the client since all errors are downcast to `ServerFnError`.
//!
//! ## Calling Server Functions from the Client
//!
//! Server functions can be called from the client by simply importing the function and calling it
//! like a normal Rust function. Unlike regular axum handlers, Dioxus server functions have a few
//! non-obvious restrictions.
//!
//! Most importantly, the arguments to the server function must implement either `Deserialize` *or*
//! `IntoRequest`. The `IntoRequest` trait is a Dioxus abstraction that represents the "inverse" of the
//! Axum `FromRequest` trait. Anything that is sent to the server from the client must be both extractable
//! with `FromRequest` on the server *and* constructible with `IntoRequest` on the client.
//!
//! Types like `WebsocketOptions` implement `IntoRequest` and pass along things like upgrade headers
//! to the server so that the server can properly upgrade the connection.
//!
//! When receiving data from the server, the return type must implement `Deserialize` *or* `FromResponse`.
//! The `FromResponse` trait is the inverse of Axum's `IntoResponse` trait, and is implemented
//! for types like `Websocket` where the raw HTTP response is needed to complete the construction
//! of the type.
//!
//! ## Server-only Extractors
//!
//! Because the arguments of the server function define the structure of the public API, some extractors
//! might not make sense to expose directly, nor would they be possible to construct on the client.
//! For example, on the web, you typically don't work directly with cookies since the browser handles
//! them for you. In these cases, the client would omit the `Cookie` header entirely, and we would need
//! "hoist" our extractor into a "server-only extractor".
//!
//! Server-only extractors are function arguments placed after the path in the `#[get]`/`#[post]` attribute.
//! These arguments are extracted on the server, but not passed in from the client. This lets the
//! server function remain callable from the client, while still allowing full access to axum's
//! extractors.
//!
//! ```
//! #[post("/api/authenticate", auth: AuthCookie)]
//! async fn authenticate() -> Result<User> { /* ... */ }
//! ```
//!
//! ## Automatic Registration
//!
//! Unlike axum handlers, server functions do not need to be manually registered with a router.
//! By default, *all* server functions in your app will be automatically registered with the
//! server when you call `dioxus::launch` or create a router manually with `dioxus::server::router()`.
//!
//! However, not all server functions are automatically registered by default. Server functions that
//! take a `State<T>` extractor cannot be automatically added to the router since the dioxus router
//! type does not know how to construct the `T` type.
//!
//! These server functions will be registered once the `ServerFnState<T>` layer is added to the app with
//! `router = router.layer(ServerFnState::new(your_state))`.
//!
//! ## Middleware
//!
//! Middleware can be added to server functions using the `#[middleware(MiddlewareType)]` attribute.
//! Middleware will be applied in the order they are specified, and will be applied before any
//! server-only extractors.
//!
//! To add router-level middleware, you can customize the axum `Router` using layers and extensions
//! as you would in a normal axum app.
//!
//! ## Anonymous Server Functions
//!
//! The `#[server]` attribute can be used without a path to create an anonymous server function.
//! These functions are still exposed as HTTP endpoints, but their names are procedurally generated
//! from the module path, function name, and a hash of the function signature. This makes it hard to
//! call these functions with `curl` or `postman`, but save you the trouble of coming up with unique
//! names for simple functions that are only called from your Dioxus app.
//!
//! If you're shipping desktop/mobile apps, we don't recommend using anonymous server functions
//! since the function names could change between builds and thus make older versions of your app
//! incompatible with newer versions of your server.
//!
//! ## Cross-platform Clients
//!
//! Server functions can be called from any platform (web, desktop mobile, etc) and use the best
//! underlying `fetch` implementation available.
//!
//! ## More examples
//!
//! With Dioxus Fullstack 0.7, pretty much anything you can do with an Axum handler, you can do with
//! a server function. More advanced examples can be found in this folder showcasing custom data types,
//! error handling, websockets, and more.

use axum_core::response::IntoResponse;
use dioxus::prelude::*;
use dioxus_fullstack::FromResponse;
use dioxus_fullstack::http::StatusCode;
use serde::{Deserialize, Serialize};

fn main() {
dioxus::launch(app);
}

fn app() -> Element {
let mut echo_action = use_action(echo);
let mut chat_action = use_action(chat);
let mut dog_data = use_action(get_data);
let mut custom_data = use_action(get_custom_data);
let mut anonymous_action = use_action(anonymous);
let mut custom_anonymous_action = use_action(custom_anonymous);

rsx! {
h1 { "Server Functions Example" }
div {
display: "flex",
flex_direction: "column",
gap: "8px",

button { onclick: move |_| echo_action.call("Hello from client".into()), "Echo: Hello" }
button { onclick: move |_| chat_action.call(42u32, Some(7u32)), "Chat (user 42, room 7)" }
button { onclick: move |_| dog_data.call(), "Get dog data" }
button { onclick: move |_| custom_data.call(), "Get custom data" }
button { onclick: move |_| anonymous_action.call(), "Call anonymous" }
button { onclick: move |_| custom_anonymous_action.call(), "Call custom anonymous" }

button {
onclick: move |_| {
echo_action.reset();
chat_action.reset();
dog_data.reset();
custom_data.reset();
anonymous_action.reset();
custom_anonymous_action.reset();
},
"Clear results"
}

pre { "Echo result: {echo_action.result():#?}" }
pre { "Chat result: {chat_action.result():#?}" }
pre { "Dog data: {dog_data.result():#?}" }
pre { "Custom data: {custom_data.result():#?}" }
pre { "Anonymous: {anonymous_action.result():#?}" }
pre { "Custom anonymous: {custom_anonymous_action.result():#?}" }
}
}
}

/// A plain server function at a `POST` endpoint that takes a string and returns it.
/// Here, we use the `Result` return type which is an alias to `Result<T, anyhow::Error>`.
#[post("/api/echo")]
async fn echo(body: String) -> Result<String> {
Ok(body)
}

/// A Server function that takes path and query parameters, as well as a server-only extractor.
#[post("/api/{user_id}/chat?room_id", headers: dioxus_fullstack::HeaderMap)]
async fn chat(user_id: u32, room_id: Option<u32>) -> Result<String> {
Ok(format!(
"User ID: {}, Room ID: {} - Headers: {:#?}",
user_id,
room_id.map_or("None".to_string(), |id| id.to_string()),
headers
))
}

/// A plain server function at a `GET` endpoint that returns some JSON data. Because `DogData` is
/// `Serialize` and `Deserialize`, it can be sent to the client as JSON automatically.
///
/// You can `curl` this endpoint and it will return a 200 status code with a JSON body:
///
/// ```json
/// {
/// "name": "Fido",
/// "age": 4
/// }
/// ```
#[get("/api/dog")]
async fn get_data() -> Result<DogData> {
Ok(DogData {
name: "Fido".to_string(),
age: 4,
})
}

#[derive(Serialize, Deserialize, Debug)]
struct DogData {
name: String,
age: u8,
}

/// A server function that returns a custom struct as JSON.
#[get("/api/custom")]
async fn get_custom_data() -> Result<CustomData> {
Ok(CustomData {
message: "Hello from the server!".to_string(),
})
}

#[derive(Debug)]
struct CustomData {
message: String,
}
impl IntoResponse for CustomData {
fn into_response(self) -> axum_core::response::Response {
axum_core::response::Response::builder()
.status(StatusCode::ACCEPTED)
.body(serde_json::to_string(&self.message).unwrap().into())
.unwrap()
}
}

impl FromResponse for CustomData {
async fn from_response(res: dioxus_fullstack::ClientResponse) -> Result<Self, ServerFnError> {
let message = res.json::<String>().await?;
Ok(CustomData { message })
}
}

/// An anonymous server function - the url path is generated from the module path and function name.
///
/// This will end up as `/api/anonymous_<hash>` where `<hash>` is a hash of the function signature.
#[server]
async fn anonymous() -> Result<String> {
Ok("Hello from an anonymous server function!".to_string())
}

/// An anonymous server function with a custom prefix and a fixed endpoint name.
///
/// This is less preferred over the `#[get]`/`#[post]` syntax but is still functional for backwards
/// compatibility. Previously, only the `#[server]` attribute was available, but as of Dioxus 0.7,
/// the `#[get]`/`#[post]` attributes are preferred for new code.
#[server(prefix = "/api/custom", endpoint = "my_anonymous")]
async fn custom_anonymous() -> Result<String> {
Ok("Hello from a custom anonymous server function!".to_string())
}
Loading
Loading