> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/tokio-rs/axum/llms.txt
> Use this file to discover all available pages before exploring further.

# Forms and multipart

> Handle form submissions and file uploads in Axum applications

Axum provides extractors for handling both URL-encoded forms and multipart form data (commonly used for file uploads).

## URL-encoded forms

The `Form` extractor deserializes form data from requests:

### Basic form handling

<Steps>
  <Step title="Define your form data structure">
    Create a struct that implements `Deserialize`:

    ```rust theme={null}
    use serde::Deserialize;

    #[derive(Deserialize)]
    struct SignUpForm {
        username: String,
        email: String,
        password: String,
    }
    ```
  </Step>

  <Step title="Use the Form extractor">
    Extract form data in your handler:

    ```rust theme={null}
    use axum::{Form, response::Html};

    async fn accept_form(Form(input): Form<SignUpForm>) -> Html<String> {
        Html(format!(
            "Welcome, {}! We'll send confirmation to {}",
            input.username, input.email
        ))
    }
    ```
  </Step>

  <Step title="Create routes">
    Set up GET and POST routes for your form:

    ```rust theme={null}
    use axum::{routing::get, Router};

    let app = Router::new()
        .route("/signup", get(show_form).post(accept_form));
    ```
  </Step>
</Steps>

### Form data sources

The `Form` extractor automatically handles data from different sources based on the request method:

<CodeGroup>
  ```rust GET/HEAD requests theme={null}
  // Form data comes from query string
  // GET /search?q=axum&page=1

  #[derive(Deserialize)]
  struct SearchQuery {
      q: String,
      page: Option<u32>,
  }

  async fn search(Form(query): Form<SearchQuery>) -> String {
      format!("Searching for '{}' on page {}", query.q, query.page.unwrap_or(1))
  }
  ```

  ```rust POST/PUT/PATCH requests theme={null}
  // Form data comes from request body
  // Requires Content-Type: application/x-www-form-urlencoded

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

  async fn login(Form(creds): Form<LoginForm>) -> String {
      format!("Logging in user: {}", creds.username)
  }
  ```
</CodeGroup>

<Note>
  For POST requests, the Form extractor requires the `Content-Type` header to be `application/x-www-form-urlencoded`. Otherwise, it will reject the request with a 415 Unsupported Media Type error.
</Note>

### Complete form example

```rust theme={null}
use axum::{
    extract::Form,
    response::Html,
    routing::get,
    Router,
};
use serde::Deserialize;

#[derive(Deserialize, Debug)]
struct ContactForm {
    name: String,
    email: String,
    message: String,
}

async fn show_form() -> Html<&'static str> {
    Html(r#"
        <!doctype html>
        <html>
            <body>
                <form action="/contact" method="post">
                    <label>
                        Name:
                        <input type="text" name="name" required>
                    </label>
                    <label>
                        Email:
                        <input type="email" name="email" required>
                    </label>
                    <label>
                        Message:
                        <textarea name="message" required></textarea>
                    </label>
                    <button type="submit">Send</button>
                </form>
            </body>
        </html>
    "#)
}

async fn handle_form(Form(form): Form<ContactForm>) -> Html<String> {
    println!("Received: {form:?}");
    Html(format!("<h1>Thank you, {}!</h1>", form.name))
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/contact", get(show_form).post(handle_form));

    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
        .await
        .unwrap();
    axum::serve(listener, app).await;
}
```

## Form responses

You can also use `Form` to return form-encoded data:

```rust theme={null}
use axum::Form;
use serde::Serialize;

#[derive(Serialize)]
struct ApiResponse {
    success: bool,
    message: String,
}

async fn api_handler() -> Form<ApiResponse> {
    Form(ApiResponse {
        success: true,
        message: "Operation completed".to_string(),
    })
}
```

This will automatically set `Content-Type: application/x-www-form-urlencoded`.

## Multipart form data

For file uploads, use the `Multipart` extractor:

### Basic file upload

```rust theme={null}
use axum::{
    extract::Multipart,
    response::Html,
    routing::get,
    Router,
};

async fn show_upload_form() -> Html<&'static str> {
    Html(r#"
        <!doctype html>
        <html>
            <body>
                <form action="/upload" method="post" enctype="multipart/form-data">
                    <label>
                        Upload file:
                        <input type="file" name="file" multiple>
                    </label>
                    <button type="submit">Upload</button>
                </form>
            </body>
        </html>
    "#)
}

async fn accept_upload(mut multipart: Multipart) {
    while let Some(field) = multipart.next_field().await.unwrap() {
        let name = field.name().unwrap().to_string();
        let file_name = field.file_name().unwrap().to_string();
        let content_type = field.content_type().unwrap().to_string();
        let data = field.bytes().await.unwrap();

        println!(
            "Field: {name}, File: {file_name}, Type: {content_type}, Size: {} bytes",
            data.len()
        );
    }
}

let app = Router::new()
    .route("/upload", get(show_upload_form).post(accept_upload));
```

### Processing multipart fields

<CodeGroup>
  ```rust Reading as bytes theme={null}
  // Get entire field contents as bytes
  while let Some(field) = multipart.next_field().await.unwrap() {
      let data = field.bytes().await.unwrap();
      println!("Received {} bytes", data.len());
  }
  ```

  ```rust Reading as text theme={null}
  // Get field contents as text
  while let Some(field) = multipart.next_field().await.unwrap() {
      if let Ok(text) = field.text().await {
          println!("Text field: {text}");
      }
  }
  ```

  ```rust Streaming chunks theme={null}
  // Stream field data in chunks
  while let Some(mut field) = multipart.next_field().await.unwrap() {
      while let Some(chunk) = field.chunk().await.unwrap() {
          // Process chunk by chunk (useful for large files)
          println!("Chunk size: {}", chunk.len());
      }
  }
  ```
</CodeGroup>

### Saving uploaded files

```rust theme={null}
use axum::extract::Multipart;
use std::io::Write;
use tokio::fs::File;
use tokio::io::AsyncWriteExt;

async fn save_file(mut multipart: Multipart) -> String {
    while let Some(field) = multipart.next_field().await.unwrap() {
        let file_name = field.file_name().unwrap().to_string();
        let data = field.bytes().await.unwrap();

        // Save to disk
        let path = format!("./uploads/{}", file_name);
        tokio::fs::write(&path, &data).await.unwrap();
        
        return format!("Saved file: {file_name}");
    }
    "No file uploaded".to_string()
}
```

<Warning>
  Always validate and sanitize file names before saving to prevent directory traversal attacks.
</Warning>

### Handling mixed form fields

Multipart forms can contain both files and regular fields:

```rust theme={null}
use axum::extract::Multipart;

#[derive(Debug)]
struct FileUpload {
    title: String,
    description: String,
    file_data: Vec<u8>,
    file_name: String,
}

async fn handle_mixed_form(mut multipart: Multipart) -> String {
    let mut upload = FileUpload {
        title: String::new(),
        description: String::new(),
        file_data: Vec::new(),
        file_name: String::new(),
    };

    while let Some(field) = multipart.next_field().await.unwrap() {
        let name = field.name().unwrap().to_string();
        
        match name.as_str() {
            "title" => upload.title = field.text().await.unwrap(),
            "description" => upload.description = field.text().await.unwrap(),
            "file" => {
                upload.file_name = field.file_name().unwrap().to_string();
                upload.file_data = field.bytes().await.unwrap().to_vec();
            }
            _ => {}
        }
    }

    format!(
        "Uploaded '{}': {} ({} bytes)",
        upload.title,
        upload.file_name,
        upload.file_data.len()
    )
}
```

## File size limits

By default, Axum limits request bodies to 2MB. Configure this for file uploads:

```rust theme={null}
use axum::{
    extract::DefaultBodyLimit,
    Router,
};
use tower_http::limit::RequestBodyLimitLayer;

let app = Router::new()
    .route("/upload", post(accept_upload))
    // Disable default limit
    .layer(DefaultBodyLimit::disable())
    // Set custom limit (250 MB)
    .layer(RequestBodyLimitLayer::new(250 * 1024 * 1024));
```

<Info>
  The `DefaultBodyLimit::disable()` must come before `RequestBodyLimitLayer` in the middleware chain.
</Info>

## Form validation

Integrate with validation libraries like `validator`:

```rust theme={null}
use axum::{Form, http::StatusCode, response::IntoResponse};
use serde::Deserialize;
use validator::Validate;

#[derive(Deserialize, Validate)]
struct NewUser {
    #[validate(length(min = 3, max = 20))]
    username: String,
    
    #[validate(email)]
    email: String,
    
    #[validate(length(min = 8))]
    password: String,
}

async fn create_user(Form(user): Form<NewUser>) -> impl IntoResponse {
    match user.validate() {
        Ok(_) => {
            // Process valid user
            (StatusCode::CREATED, "User created")
        }
        Err(e) => {
            // Return validation errors
            (StatusCode::BAD_REQUEST, format!("Validation error: {e}"))
        }
    }
}
```

## Testing form handlers

```rust theme={null}
#[cfg(test)]
mod tests {
    use super::*;
    use axum::{
        body::Body,
        http::{self, Request, StatusCode},
    };
    use http_body_util::BodyExt;
    use tower::ServiceExt;

    #[tokio::test]
    async fn test_form_submission() {
        let app = Router::new()
            .route("/", post(accept_form));

        let response = app
            .oneshot(
                Request::post("/")
                    .header(
                        http::header::CONTENT_TYPE,
                        mime::APPLICATION_WWW_FORM_URLENCODED.as_ref(),
                    )
                    .body(Body::from("username=john&email=john@example.com"))
                    .unwrap(),
            )
            .await
            .unwrap();

        assert_eq!(response.status(), StatusCode::OK);
        
        let body = response.into_body().collect().await.unwrap().to_bytes();
        let body_str = std::str::from_utf8(&body).unwrap();
        assert!(body_str.contains("john"));
    }
}
```

<Accordion title="Common form issues and solutions">
  **Missing Content-Type header**: POST forms require `Content-Type: application/x-www-form-urlencoded`. Check your client is sending this header.

  **Form extractor must be last**: If you have multiple extractors, `Form` and `Multipart` must come last since they consume the request body.

  **Field name mismatch**: Ensure your struct field names match the HTML form field names, or use serde's `#[serde(rename = "...")]` attribute.

  **Large file uploads fail**: Increase the body size limit using `DefaultBodyLimit` and `RequestBodyLimitLayer`.
</Accordion>
