https://github.com/codeitlikemiley/fullstack-axum-dioxus
https://github.com/codeitlikemiley/fullstack-axum-dioxus
Last synced: 2 months ago
JSON representation
- Host: GitHub
- URL: https://github.com/codeitlikemiley/fullstack-axum-dioxus
- Owner: codeitlikemiley
- Created: 2024-10-20T22:47:48.000Z (over 1 year ago)
- Default Branch: main
- Last Pushed: 2025-09-20T01:40:43.000Z (6 months ago)
- Last Synced: 2025-09-20T03:28:32.351Z (6 months ago)
- Language: Rust
- Size: 24.4 KB
- Stars: 6
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# Dioxus Fullstack (Island Architecture)
## TODO :
- see if we can use rsx on `components` crate so we dont have to write web-sys to access DOM elements
Screenshot

## Requirements
- geni cli
- dx cli
- wasm-bindgen-cli
- wasm-pack
- cargo-runner
## Initialize the app
1. mkdir hexagonal
2. ws init
3. add new members on `Cargo.toml`
```toml
[workspace]
resolver = "2"
members = ["server", "components", "pages"]
```
4. copy [`.gitignore` ](https://gist.github.com/codeitlikemiley/f4b405d7afe8b76d7ce799c1732649db)
5. generate crates
```
cargo new server
cargo new components --lib
cargo new pages --lib
```
6. init
## Set migrations
1. export DATABASE_URL=postgres://postgres:postgres@localhost:5432/hexagonal
2. geni create
3. geni new create_users_table
4. geni up
## Add DB Queries
1. cargo init --lib db
2. cd db
3. mkdir queries
4. add initial queries
e.g. `users.sql`
```
--: User()
--! get_users : User
SELECT
id,
email
FROM users;
```
5. touch build.rs
build.rs
```rust
use std::env;
use std::path::Path;
fn main() {
// Compile our SQL
cornucopia();
}
fn cornucopia() {
// For the sake of simplicity, this example uses the defaults.
let queries_path = "queries";
let out_dir = env::var_os("OUT_DIR").unwrap();
let file_path = Path::new(&out_dir).join("cornucopia.rs");
let db_url = env::var_os("DATABASE_URL").unwrap();
// Rerun this build script if the queries or migrations change.
println!("cargo:rerun-if-changed={queries_path}");
// Call cornucopia. Use whatever CLI command you need.
let output = std::process::Command::new("cornucopia")
.arg("-q")
.arg(queries_path)
.arg("--serialize")
.arg("-d")
.arg(&file_path)
.arg("live")
.arg(db_url)
.output()
.unwrap();
// If Cornucopia couldn't run properly, try to display the error.
if !output.status.success() {
panic!("{}", &std::str::from_utf8(&output.stderr).unwrap());
}
}
```
6. Add `db` dependencies
```sh
cargo add cornucopia_async@0.6
cargo add tokio-postgres@0.7
cargo add deadpool-postgres@0.12
cargo add postgres-types@0.2
cargo add tokio@1 --features macros,rt-multi-thread
cargo add futures@0.3
cargo add serde@1 --features derive
```
7. modify `lib.rs`
lib.rs
```rust
use std::str::FromStr;
pub use cornucopia_async::Params;
pub use deadpool_postgres::{Pool, PoolError, Transaction};
pub use tokio_postgres::Error as TokioPostgresError;
pub use queries::users::User;
pub fn create_pool(database_url: &str) -> deadpool_postgres::Pool {
let config = tokio_postgres::Config::from_str(database_url).unwrap();
let manager = deadpool_postgres::Manager::new(config, tokio_postgres::NoTls);
deadpool_postgres::Pool::builder(manager).build().unwrap()
}
include!(concat!(env!("OUT_DIR"), "/cornucopia.rs"));
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn load_users() {
let db_url = std::env::var("DATABASE_URL").unwrap();
let pool = create_pool(&db_url);
let client = pool.get().await.unwrap();
//let transaction = client.transaction().await.unwrap();
let users = crate::queries::users::get_users()
.bind(&client)
.all()
.await
.unwrap();
dbg!(users);
}
}
```
8. build the crate and run the tests using cargo-runner
## Set up server crate
1. cd server
2. touch src/config.rs
config.rs
```rust
#[derive(Clone, Debug)]
pub struct Config {
pub database_url: String,
}
impl Config {
pub fn new() -> Config {
let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL not set");
Config { database_url }
}
}
```
3. touch src/errors.rs
errors.rs
```rust
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
};
use db::{PoolError, TokioPostgresError};
use std::fmt;
#[derive(Debug)]
pub enum CustomError {
FaultySetup(String),
Database(String),
}
// Allow the use of "{}" format specifier
impl fmt::Display for CustomError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
CustomError::FaultySetup(ref cause) => write!(f, "Setup Error: {}", cause),
//CustomError::Unauthorized(ref cause) => write!(f, "Setup Error: {}", cause),
CustomError::Database(ref cause) => {
write!(f, "Database Error: {}", cause)
}
}
}
}
// So that errors get printed to the browser?
impl IntoResponse for CustomError {
fn into_response(self) -> Response {
let (status, error_message) = match self {
CustomError::Database(message) => (StatusCode::UNPROCESSABLE_ENTITY, message),
CustomError::FaultySetup(message) => (StatusCode::UNPROCESSABLE_ENTITY, message),
};
format!("status = {}, message = {}", status, error_message).into_response()
}
}
impl From for CustomError {
fn from(err: axum::http::uri::InvalidUri) -> CustomError {
CustomError::FaultySetup(err.to_string())
}
}
impl From for CustomError {
fn from(err: TokioPostgresError) -> CustomError {
CustomError::Database(err.to_string())
}
}
impl From for CustomError {
fn from(err: PoolError) -> CustomError {
CustomError::Database(err.to_string())
}
}
```
4. Add dependencies
```sh
cargo add axum@0.7 --no-default-features -F json,http1,tokio
cargo add tokio@1 --no-default-features -F macros,fs,rt-multi-thread
cargo add --path ../db
```
5. update `main.rs`
main.rs
```rust
mod config;
mod errors;
use crate::errors::CustomError;
use axum::{extract::Extension, response::Json, routing::get, Router};
use db::User;
use std::net::SocketAddr;
#[tokio::main]
async fn main() {
let config = config::Config::new();
let pool = db::create_pool(&config.database_url);
// build our application with a route
let app = Router::new()
.route("/", get(users))
.layer(Extension(config))
.layer(Extension(pool.clone()));
// run it
let addr = SocketAddr::from(([0, 0, 0, 0], 3000));
println!("listening on {}", addr);
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
axum::serve(listener, app.into_make_service())
.await
.unwrap();
}
async fn users(Extension(pool): Extension) -> Result>, CustomError> {
let client = pool.get().await?;
let users = db::queries::users::get_users().bind(&client).all().await?;
Ok(Json(users))
}
```
6. run the server
## Set up Pages crate
1. cargo init --lib pages
2. cd pages
3. install dependencies
```sh
cargo add dioxus
cargo adddioxus-ssr
cargo add --path ../db
```
4. create `src/layout.rs`
layout.rs
```rust
#![allow(non_snake_case)]
use dioxus::prelude::*;
#[component]
pub fn Layout(title: String, children: Element) -> Element {
rsx!(
head {
title { "{title}" }
meta { charset: "utf-8" }
meta { "http-equiv": "X-UA-Compatible", content: "IE=edge" }
meta {
name: "viewport",
content: "width=device-width, initial-scale=1"
}
}
body { {children} }
)
}
```
5. create `src/users.rs`
users.rs
```rust
use crate::layout::Layout;
use db::User;
use dioxus::prelude::*;
// Define the properties for IndexPage
#[derive(Props, Clone, PartialEq)] // Add Clone and PartialEq here
pub struct IndexPageProps {
pub users: Vec,
}
// Define the IndexPage component
#[component]
pub fn IndexPage(props: IndexPageProps) -> Element {
rsx! {
Layout { title: "Users Table",
table {
thead {
tr {
th { "ID" }
th { "Email" }
}
}
tbody {
for user in props.users {
tr {
td {
strong { "{user.id}" }
}
td { "{user.email}" }
}
}
}
}
}
}
}
```
6. update `src/lib.rs`
lib.rs
```rust
mod layout;
pub mod users;
use dioxus::prelude::*;
pub fn render(mut virtual_dom: VirtualDom) -> String {
virtual_dom.rebuild_in_place();
let html = dioxus_ssr::render(&virtual_dom);
format!("{}", html)
}
```
7. cd to `server` crate
8. update dependencies
```sh
cargo add dioxus
cargo add --path ../pages
```
9. update `main.rs`
main.rs
```rust
mod config;
mod errors;
use crate::errors::CustomError;
use axum::response::Html;
use axum::{extract::Extension, routing::get, Router};
use dioxus::dioxus_core::VirtualDom;
use pages::{
render,
users::{IndexPage, IndexPageProps},
};
use std::net::SocketAddr;
#[tokio::main]
async fn main() {
let config = config::Config::new();
let pool = db::create_pool(&config.database_url);
// build our application with a route
let app = Router::new()
.route("/", get(users))
.layer(Extension(config))
.layer(Extension(pool.clone()));
// run it
let addr = SocketAddr::from(([0, 0, 0, 0], 3000));
println!("listening on... {}", addr);
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
axum::serve(listener, app.into_make_service())
.await
.unwrap();
}
pub async fn users(Extension(pool): Extension) -> Result, CustomError> {
let client = pool.get().await?;
let users = db::queries::users::get_users().bind(&client).all().await?;
let html = render(VirtualDom::new_with_props(
IndexPage,
IndexPageProps { users },
));
Ok(Html(html))
}
```
10. run the server
## Set up `assets` crate for static files
1. cargo init --lib assets
2. cd assets
3. mkdir images
4. create an `avatar.svg` file on `images` folder
avatar.svg
```svg
```
5. touch `build.rs`
6. update `build.rs`
build.rs
```rust
use ructe::{Result, Ructe};
fn main() -> Result<()> {
let mut ructe = Ructe::from_env().unwrap();
let mut statics = ructe.statics().unwrap();
statics.add_files("images").unwrap();
ructe.compile_templates("images").unwrap();
Ok(())
}
```
7. add dependencies
```sh
cargo add mime@0.3
cargo add --build ructe@0.17 --no-default-features -F mime03
```
8. update `lib.rs`
lib.rs
```rust
include!(concat!(env!("OUT_DIR"), "/templates.rs"));
pub use templates::statics as files;
```
9. cd to `server` crate
10. create `static_files.rs`
11. update `static_files.rs`
static_files.rs
```rust
use assets::templates::statics::StaticFile;
use axum::body::Body;
use axum::extract::Path;
use axum::http::{header, HeaderValue, Response, StatusCode};
use axum::response::IntoResponse;
pub async fn static_path(Path(path): Path) -> impl IntoResponse {
let path = path.trim_start_matches('/');
if let Some(data) = StaticFile::get(path) {
Response::builder()
.status(StatusCode::OK)
.header(
header::CONTENT_TYPE,
HeaderValue::from_str(data.mime.as_ref()).unwrap(),
)
.body(Body::from(data.content))
.unwrap()
} else {
Response::builder()
.status(StatusCode::NOT_FOUND)
.body(Body::empty())
.unwrap()
}
}
```
12. modify `main.rs` to add the new route for static files
main.rs
```rust
// load module
mod static_files;
let app = Router::new()
.route("/", get(users))
.route("/static/*path", get(static_files::static_path)) // add this line
.layer(Extension(config))
.layer(Extension(pool.clone()));
...
```
13. cargo add --path ../assets
14. use the static files on `pages/src/users.rs`
users.rs
```rust
// use avatar
use assets::files::avatar_svg;
...
// access the static file
img {
src: format!("/static/{}", avatar_svg.name),
width: "16",
height: "16"
}
```
15. run the server
## Set up Components crate
1. cargo init --lib components
2. add dependencies
```toml
dioxus = "0.5.6"
js-sys = "0.3.72"
wasm-bindgen = "0.2.93"
web-sys = { version = "0.3.72", features = ["Document", "Element", "HtmlElement", "Window", "console"] }
```
3. set the crate type to `cdylib` and `rlib`
4. add example code to test on `src/lib.rs`
lib.rs
```rust
use js_sys::Math;
use wasm_bindgen::prelude::*;
use web_sys::{console, window, Element};
#[wasm_bindgen]
pub fn say_hello() {
let random_number = Math::random();
let message = format!("Hello from Rust! Random number: {}", random_number);
// Log to the browser console
console::log_1(&"Logging to console from Rust!".into());
console::log_1(&format!("Generated random number: {}", random_number).into());
// Show alert
web_sys::window()
.unwrap()
.alert_with_message(&message)
.unwrap();
}
#[wasm_bindgen(start)]
pub fn start() -> Result<(), JsValue> {
// Access the DOM window object
let window = window().unwrap();
let document = window.document().unwrap();
// Get the button element by ID
let button: Element = document.get_element_by_id("alert-btn").unwrap();
// Set an event listener for the button click
let closure = Closure::wrap(Box::new(move || {
// Call the Rust function say_hello
say_hello();
}) as Box);
// Set an event listener for the button click
button
.dyn_ref::()
.unwrap()
.set_onclick(Some(closure.as_ref().unchecked_ref()));
// We need to keep the closure alive, so we store it in memory.
closure.forget();
Ok(())
}
```
4. cd to assets crate
5. create `js/pages/users` folder
6. go back to `components` crate
7. generate assets using command
```sh
wasm-pack build --target web --out-dir ../assets/js/pages/users
```
8. Use the generated asset on `pages/src/users.rs`
```rust
script {
r#type: "module",
dangerous_inner_html: r#"
import init from '/static/components.js';
init();
"#
}
```
### Feature gating for wasm components
we need to use feature gating to only include components that are needed
```sh
wasm-pack build --target web --out-dir ../assets/js/pages/${feature} --features ${feature}
```
e.g.
```toml
[features]
default = []
users = []
featurex = []
```
on rust code we can do
```rust
#[cfg(feature = "feature1")]
#[wasm_bindgen]
fn some_function() {
// Implementation for feature1
}
```