rust
46 lines · 8 steps
Validating a signup form in Axum
An Axum handler parses a submitted form, collects per-field validation errors, and returns either 422 or 201.
Explained by
highlit
1use axum::{extract::Form, http::StatusCode, response::{IntoResponse, Response}, Json};
2use serde::Deserialize;
3use serde_json::json;
4use std::collections::HashMap;
5
6#[derive(Deserialize)]
7pub struct SignupForm {
8 username: String,
9 email: String,
10 password: String,
11 age: Option<u32>,
12}
13
14pub async fn signup(Form(form): Form<SignupForm>) -> Response {
15 let mut errors: HashMap<&'static str, Vec<String>> = HashMap::new();
16
17 if form.username.trim().len() < 3 {
18 errors.entry("username").or_default().push("must be at least 3 characters".into());
19 }
20 if !form.username.chars().all(|c| c.is_alphanumeric() || c == '_') {
21 errors.entry("username").or_default().push("may only contain letters, digits and underscores".into());
22 }
23
24 if !form.email.contains('@') || !form.email.contains('.') {
25 errors.entry("email").or_default().push("is not a valid email address".into());
26 }
27
28 if form.password.len() < 8 {
29 errors.entry("password").or_default().push("must be at least 8 characters".into());
30 }
31 if !form.password.chars().any(|c| c.is_ascii_digit()) {
32 errors.entry("password").or_default().push("must contain a digit".into());
33 }
34
35 if let Some(age) = form.age {
36 if age < 13 {
37 errors.entry("age").or_default().push("must be at least 13".into());
38 }
39 }
40
41 if !errors.is_empty() {
42 return (StatusCode::UNPROCESSABLE_ENTITY, Json(json!({ "errors": errors }))).into_response();
43 }
44
45 (StatusCode::CREATED, Json(json!({ "username": form.username }))).into_response()
46}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Accumulating errors in a map lets you report every problem at once instead of failing on the first.
- 2Deriving Deserialize on a struct turns raw form bytes into typed, validated fields for free.
- 3Returning a (StatusCode, Json) tuple is the idiomatic way to pair a status with a body in Axum.
Related explainers
rust
use axum::{extract::{Path, State}, http::StatusCode, Json}; use dashmap::DashMap; use serde::Serialize; use std::sync::Arc;
Request coalescing in an Axum handler
caching
concurrency
request-coalescing
Advanced
8 steps
ruby
class TemplateInterpolator PLACEHOLDER = /\{\{\s*([\w.]+)\s*\}\}/ def initialize(strict: false)
Interpolating templates with dotted keys in Ruby
regex
string-interpolation
hash-traversal
Intermediate
6 steps
go
package config import ( "fmt"
A thread-safe config singleton in Go
singleton
concurrency
environment-variables
Intermediate
7 steps
rust
use std::net::Ipv4Addr; use std::str::FromStr; #[derive(Debug, Clone, Copy)]
Parsing and matching IPv4 CIDR ranges in Rust
bitwise-operations
parsing
error-handling
Intermediate
8 steps
rust
use std::sync::OnceLock; static CRC_TABLE: OnceLock<[u32; 256]> = OnceLock::new();
Table-driven CRC32 with lazy init in Rust
checksums
lazy-initialization
lookup-tables
Intermediate
8 steps
typescript
import { parsePhoneNumberFromString, CountryCode } from 'libphonenumber-js'; export interface NormalizedPhone { e164: string;
Normalizing phone numbers to E.164 in TypeScript
validation
normalization
error-handling
Intermediate
7 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/validating-a-signup-form-in-axum-explained-rust-d3fe/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.