Forms (Form Extractor)

1. Welcome and Icebreaker:

Have you ever built a form for a website? What was it for? If you could design any form for a web app, what would it collect from users?

2. Outline the Session:

Agenda:

  1. Concept Explanation: Handling form data with the Form extractor in Axum.
  2. Live Coding: Accept and parse form data in a handler, and discuss validation and error handling.
  3. Tasks: Practice by building your own form endpoints and adding validation.

Learning Outcomes:

  • Use the Form extractor to parse application/x-www-form-urlencoded data.
  • Define and deserialize Rust structs for form data.
  • Add basic validation and error handling for form submissions.

Expectations: Try sending form data to your server and see how it works! Ask questions and experiment with different field values.

3. Concept Explanation:

The Form extractor in Axum allows you to accept and parse form data (like from HTML forms) into Rust structs. Your struct must derive serde::Deserialize. This is useful for endpoints that handle form submissions, such as login or registration forms.

When a client submits a form with the application/x-www-form-urlencoded content type, Axum will automatically parse the request body and attempt to deserialize it into your struct. If the data is missing required fields or is malformed, Axum will return a 400 Bad Request error by default.

Why use the Form extractor?

  • It provides a type-safe way to handle form data.
  • It integrates seamlessly with Rust’s type system and Serde for deserialization.
  • It helps you avoid manual parsing and error-prone string manipulation.

Validation: While the extractor ensures the data is present and well-formed, you should still validate the content (e.g., check if an email is valid, or if a password is long enough). This can be done after extraction, in your handler logic.

Error Handling: If deserialization fails (e.g., missing fields), Axum will automatically reject the request. For custom validation, you can return your own error responses.

4. Live Coding Demonstration:

Let’s see how to accept and process form data in Axum.

use axum::{extract::Form, Router, routing::post, response::IntoResponse, http::StatusCode};
use serde::Deserialize;

#[derive(Deserialize)]
struct LoginForm {
    username: String,
    password: String,
}

async fn login(Form(data): Form<LoginForm>) -> impl IntoResponse {
    // Basic validation example
    if data.username.is_empty() || data.password.is_empty() {
        return (StatusCode::BAD_REQUEST, "Username and password are required".to_string());
    }
    (StatusCode::OK, format!("Logged in as {}", data.username))
}

#[derive(Deserialize)]
struct RegisterForm {
    username: String,
    email: String,
}

async fn register(Form(data): Form<RegisterForm>) -> impl IntoResponse {
    // Simple validation
    if data.username.is_empty() || data.email.is_empty() {
        return (StatusCode::BAD_REQUEST, "Username and email are required".to_string());
    }
    if !data.email.contains('@') {
        return (StatusCode::BAD_REQUEST, "Invalid email address".to_string());
    }
    (StatusCode::OK, format!("Registered user: {} with email: {}", data.username, data.email))
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/login", post(login))
        .route("/register", post(register));
    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

Step-by-step explanation:

  • We define two structs, LoginForm and RegisterForm, each deriving Deserialize.
  • The login handler extracts form data and performs basic validation.
  • The register handler demonstrates only a minimal @ check, not complete email validation.
  • Both handlers return appropriate error messages and status codes for invalid input.
  • The router is set up with both /login and /register endpoints.

Best Practices:

  • Always validate form data before using it.
  • Use clear struct field names matching the form fields.
  • Return helpful error messages for invalid input.

Common Pitfalls:

  • Forgetting to derive Deserialize for your struct.
  • Sending data with the wrong content type (should be application/x-www-form-urlencoded).
  • Not handling missing or invalid fields.

5. Tasks

  • Add a /register endpoint that accepts a username and email via form data (see example above).
  • Try submitting a form with missing fields and handle the error gracefully.
  • Add validation logic for the form fields (e.g., check for valid email, non-empty username).
  • Bonus: Try adding a password field to the register form and validate its length (e.g., at least 8 characters).
  • Challenge: Return a custom JSON error response instead of plain text for invalid input.