Forms
Browsers have support for client side validation of form data built in. We can use this along with server side validation to give the user a nice experience and ensure security on the back end.
Browser Validation
In the following form we use an email type and a required attribute. The browser will now block form submission until the field is filled in with a valid email address and password.
<form>
<label for="user_email">Email:</label>
<input id="user_email" name="email" type="email" required>
<button>Submit</button>
</form>
We can write this same form using Dioxus. Update crates/ui-components/src/users.rs
with a form to add users.
use crate::layout::Layout;
use db::User;
use dioxus::prelude::*;
struct Props {
users: Vec<User>,
}
pub fn users(users: Vec<User>) -> String {
fn app(cx: Scope<Props>) -> Element {
cx.render(rsx! {
Layout { title: "Users Table",
table {
thead {
tr {
th { "ID" }
th { "Email" }
}
}
tbody {
cx.props.users.iter().map(|user| rsx!(
tr {
td {
strong {
"{user.id}"
}
}
td {
"{user.email}"
}
}
))
}
}
form {
action: "/sign_up",
method: "POST",
label { r#for: "user_email", "Email:" }
input { id: "user_email", name: "email", r#type: "email", required: "true" }
button { "Submit" }
}
}
})
}
let mut app = VirtualDom::new_with_props(app, Props { users });
let _ = app.rebuild();
dioxus::ssr::render_vdom(&app)
}
Note: for
and type
are Rust keywords. We must prefix them with r#
so Rust knows that we want the raw string literal of "for" and "type".
We need to install serde to transform the HTTP body into a Rust struct.
cd crates/axum-server
cargo add [email protected] --features derive
Axum has support for Handlers. We can use those in a lot of different ways and one way is for form implementations. We are going to create a create_form
handler to save new users to our database.
Update crates/axum-server/src/main.rs
mod config;
mod errors;
use crate::errors::CustomError;
use axum::{
extract::Extension,
response::Html,
response::Redirect,
routing::get,
routing::post,
Form,
Router,
};
use serde::Deserialize;
use std::net::SocketAddr;
#[tokio::main]
async fn main() {
let config = config::Config::new();
let pool = db::create_pool(&config.database_url);
let app = Router::new()
.route("/", get(users))
.route("/sign_up", post(accept_form)) .layer(Extension(config))
.layer(Extension(pool.clone()));
let addr = SocketAddr::from(([0, 0, 0, 0], 3000));
println!("listening on {}", addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
async fn users(Extension(pool): Extension<db::Pool>) -> Result<Html<String>, CustomError> {
let client = pool.get().await?;
let users = db::queries::users::get_users().bind(&client).all().await?;
Ok(Html(ui_components::users::users(users)))
}
#[derive(Deserialize )]
struct SignUp {
email: String,
}
async fn accept_form(
Extension(pool): Extension<db::Pool>,
Form(form): Form<SignUp>,
) -> Result<Redirect, CustomError> {
let client = pool.get().await?;
let email = form.email;
let hashed_password = String::from("aaaa");
let _ = db::queries::users::create_user()
.bind(&client, &email.as_str(), &hashed_password.as_str())
.await?;
Ok(Redirect::to("/"))
}
We are using db::queries::users::create_user()
in our accept_form
handler. We must also update crates/db/queries/users.sql
to include our actual SQL query
SELECT
id,
email
FROM users;
INSERT INTO users (email, hashed_password)
VALUES(:email, :hashed_password);
You should get results like the screenshot below.

If you add an email to the form and press submit, the server should handle that request and update the users table.
Server Side Validation
Our web form validates that the user input is an email. We should also check that the user input is an email on the server. We can use Validator which will allow us to add validation to the SignUp
struct.
Install the Validator
crate.
cd crates/axum-server
cargo add [email protected] --features derive
Update crates/axum-server/src/main.rs
mod config;
mod errors;
use crate::errors::CustomError;
use axum::{
extract::Extension,
http::StatusCode,
response::Html,
response::IntoResponse,
response::Redirect,
response::Response,
routing::get,
routing::post,
Form,
Router,
};
use serde::Deserialize;
use std::net::SocketAddr;
use validator::Validate;
#[tokio::main]
async fn main() {
let config = config::Config::new();
let pool = db::create_pool(&config.database_url);
let app = Router::new()
.route("/", get(users))
.route("/sign_up", post(accept_form))
.layer(Extension(config))
.layer(Extension(pool.clone()));
let addr = SocketAddr::from(([0, 0, 0, 0], 3000));
println!("listening on {}", addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
async fn users(Extension(pool): Extension<db::Pool>) -> Result<Html<String>, CustomError> {
let client = pool.get().await?;
let users = db::queries::users::get_users().bind(&client).all().await?;
Ok(Html(ui_components::users::users(users)))
}
#[derive(Deserialize, Validate)]
struct SignUp {
#[validate(email)] email: String,
}
async fn accept_form(
Extension(pool): Extension<db::Pool>,
Form(form): Form<SignUp>,
) -> Result<Response, CustomError> {
if form.validate().is_err() {
return Ok((StatusCode::BAD_REQUEST, "Bad request").into_response());
}
let client = pool.get().await?;
let email = form.email;
let hashed_password = String::from("aaaa");
let _ = db::queries::users::create_user()
.bind(&client, &email.as_str(), &hashed_password.as_str())
.await?;
Ok(Redirect::to("/").into_response()) }
And we can test that our validation works by sending a request directly to the server (bypassing the browser form):
curl http://localhost:3000/sign_up --data-raw 'email=bad-data'