# How to Implement File Uploads and Handle Multipart Form Data in Topcoat

> Learn to implement file uploads in Topcoat using the Multipart extractor. Handle multipart form data efficiently with asynchronous streaming when the multipart Cargo feature is enabled.

- Repository: [Tokio/topcoat](https://github.com/tokio-rs/topcoat)
- Tags: how-to-guide
- Published: 2026-07-21

---

**Topcoat handles multipart uploads through the `Multipart` extractor in `topcoat-router`, which streams form fields asynchronously when the `multipart` Cargo feature is enabled.**

Topcoat is a modular Rust web framework built on Tokio. Its multipart support is isolated behind a feature flag to keep core dependencies minimal, leveraging the `multer` crate for boundary parsing and streaming. When enabled, the `Multipart` type acts as a request extractor that integrates directly with Topcoat's routing macros.

## Enabling Multipart Support

Before handling file uploads, activate the multipart functionality in your [`Cargo.toml`](https://github.com/tokio-rs/topcoat/blob/main/Cargo.toml). The extractor is defined in [`crates/topcoat-router/src/content/multipart.rs`](https://github.com/tokio-rs/topcoat/blob/main/crates/topcoat-router/src/content/multipart.rs) but only compiled when the feature is present:

```toml
[dependencies]
topcoat = { version = "0.1", features = ["multipart"] }

```

Once enabled, the `Multipart` type becomes available for import from `topcoat::router`.

## The Multipart Extractor

The **`Multipart`** struct implements both `FromRequest` and `OptionalFromRequest` traits. This allows handlers to require a multipart body or accept it optionally when the request might contain other content types.

During extraction, Topcoat inspects the `Content-Type` header and parses the `boundary` token using `multer::parse_boundary` (see the `from_request` implementation in [`crates/topcoat-router/src/content/multipart.rs`](https://github.com/tokio-rs/topcoat/blob/main/crates/topcoat-router/src/content/multipart.rs)). If the header is missing or the boundary is malformed, the extractor automatically returns a **400 Bad Request** with the `invalid_boundary` error code.

## Processing Upload Fields

After extraction, iterate over form parts using `multipart.next_field().await?`. This method yields an optional `Field` struct, which provides metadata and streaming access to each part's payload:

- **`name()`** – Returns the form field name.
- **`file_name()`** – Returns the original filename when present.
- **`content_type()`** – Returns the MIME type of the part.
- **`bytes()`** – Loads the entire field into memory as `Bytes`.
- **`text()`** – Interprets the field as a UTF-8 string.
- **`chunk()`** – Streams the field chunk-by-chunk for large files.

Additionally, `Field` implements `Stream<Item = Result<Bytes>>`, allowing you to apply standard stream combinators for backpressure-aware processing.

## Complete Implementation Examples

### Basic Upload Handler

This handler receives a multipart request and prints the size of each part:

```rust
use topcoat::{
    Result,
    router::{Multipart, route},
};

#[route(POST "/api/upload")]
async fn upload(mut multipart: Multipart) -> Result<&'static str> {
    while let Some(field) = multipart.next_field().await? {
        let name = field.name().unwrap_or("<unnamed>");
        let filename = field.file_name().unwrap_or("<no-file>");
        let data = field.bytes().await?;
        println!("field `{name}` ({filename}) → {} bytes", data.len());
    }
    Ok("upload complete")
}

```

### Streaming Large Files to Disk

For production scenarios, avoid loading entire files into memory by streaming chunks directly to the filesystem:

```rust
use std::{path::PathBuf, io::Result as IoResult};
use topcoat::{router::{Multipart, route}, Result};
use tokio::fs::File;
use tokio::io::AsyncWriteExt;

#[route(POST "/files")]
async fn save_files(mut multipart: Multipart) -> Result<&'static str> {
    while let Some(mut field) = multipart.next_field().await? {
        let filename = field
            .file_name()
            .map(|s| s.to_string())
            .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());

        let mut file = File::create(PathBuf::from("uploads").join(&filename)).await?;
        while let Some(chunk) = field.chunk().await? {
            file.write_all(&chunk).await?;
        }
    }
    Ok("files stored")
}

```

### Optional Multipart Handling

When a route might receive either multipart or JSON data, use `Option<Multipart>`:

```rust
use topcoat::{
    router::{Multipart, route},
    Result,
};

#[route(POST "/maybe")]
async fn maybe_upload(multipart: Option<Multipart>) -> Result<&'static str> {
    if let Some(mut mp) = multipart {
        while let Some(field) = mp.next_field().await? {
            let data = field.bytes().await?;
            println!("received {} bytes", data.len());
        }
        Ok("processed multipart")
    } else {
        Ok("no files submitted")
    }
}

```

## Testing with cURL

Verify your implementation using standard multipart form syntax:

```bash
curl -X POST http://localhost:3000/api/upload \
     -F "photo=@/path/to/image.png" \
     -F "description=My picture"

```

This sends two parts: a file named `photo` and a text field named `description`.

## Summary

- **Enable the feature**: Add `multipart` to your `topcoat` dependency to compile the extractor.
- **Use the extractor**: Import `Multipart` from `topcoat::router` and declare it as a handler parameter.
- **Handle errors**: Topcoat returns 400 Bad Request automatically for invalid boundaries.
- **Stream efficiently**: Use `chunk()` or the `Stream` implementation to process large files without exhausting memory.
- **Make it optional**: Use `Option<Multipart>` for endpoints that accept variable content types.

## Frequently Asked Questions

### What happens if the Content-Type header is missing or malformed?

Topcoat returns a **400 Bad Request** with the error kind `invalid_boundary`. This occurs automatically during the `FromRequest` implementation in [`crates/topcoat-router/src/content/multipart.rs`](https://github.com/tokio-rs/topcoat/blob/main/crates/topcoat-router/src/content/multipart.rs) before your handler code executes.

### Can I process multipart uploads without loading the entire file into memory?

Yes. The `Field` type implements `Stream<Item = Result<Bytes>>`, allowing you to call `field.chunk().await?` in a loop or use any Tokio-compatible stream consumer. This approach is essential for handling gigabyte-scale uploads without allocating large buffers.

### Is the Multipart extractor available for all HTTP methods?

The extractor works with any HTTP method, but it is most commonly used with **POST** and **PUT** requests. The `#[route]` macro accepts standard method annotations like `POST "/upload"` regardless of the extractor type.

### How do I handle mixed form data containing both files and text fields?

The `next_field()` iterator yields each part sequentially regardless of content type. Inspect `field.file_name()` to distinguish files from text inputs, and use `field.text().await?` for form fields and `field.bytes().await?` or streaming for binary data. Both variants appear as `Field` instances during iteration.