Skip to content

Form Data Parser Tutorial

This tutorial shows you how to handle file uploads using parseFormData. You will start with the simplest case (in-memory buffering), then graduate to custom upload handlers that stream files to disk or cloud storage.

Prerequisites

  • A Remix V3 project with a route that accepts POST requests
  • Basic understanding of HTML forms

Step 1: Parse a Simple Form

The simplest usage mirrors request.formData() -- call parseFormData and read fields from the returned FormData object.

ts
import { parseFormData } from 'remix/form-data-parser'

export async function action({ request }: { request: Request }) {
  let formData = await parseFormData(request)

  let name = formData.get('name') as string
  let email = formData.get('email') as string

  // Save to database, send email, etc.
  return Response.json({ name, email })
}

This works for both URL-encoded forms (application/x-www-form-urlencoded) and multipart forms (multipart/form-data).

Step 2: Handle File Uploads

When the form includes file inputs, file fields arrive as FileUpload instances instead of plain strings.

ts
import { parseFormData, FileUpload } from 'remix/form-data-parser'

export async function action({ request }: { request: Request }) {
  let formData = await parseFormData(request)

  let title = formData.get('title') as string
  let photo = formData.get('photo') as FileUpload

  if (photo && photo.size > 0) {
    console.log(photo.name) // 'vacation.jpg'
    console.log(photo.type) // 'image/jpeg'
    console.log(photo.size) // 2097152

    // Read the file content
    let bytes = await photo.arrayBuffer()
    console.log(`Read ${bytes.byteLength} bytes`)
  }

  return Response.json({ title, fileName: photo?.name })
}

What is FileUpload? A FileUpload is similar to a standard File object. It has name, type, and size properties, plus stream(), arrayBuffer(), and text() methods. The difference is that its content comes from the request stream rather than a pre-allocated buffer.

Step 3: Use a Custom Upload Handler

Without an upload handler, file contents are buffered in memory. For production use, provide an upload handler that streams files to a permanent destination.

An upload handler is an async function that receives each FileUpload and returns a value. That return value replaces the file in the FormData object.

ts
import { parseFormData, FileUpload } from 'remix/form-data-parser'
import { writeFile } from 'remix/fs'

export async function action({ request }: { request: Request }) {
  let formData = await parseFormData(request, {}, async (fileUpload) => {
    // Generate a unique filename
    let filename = `${Date.now()}-${fileUpload.name}`
    let path = `./uploads/${filename}`

    // Stream the file to disk
    let file = new File([await fileUpload.arrayBuffer()], filename, {
      type: fileUpload.type,
    })
    await writeFile(path, file)

    // Return the path -- this becomes the value in FormData
    return path
  })

  let title = formData.get('title') as string
  let photoPath = formData.get('photo') as string // Now a path string

  return Response.json({ title, photoPath })
}

Step 4: Store Files with FileStorage

For a cleaner approach, use the file-storage package as your upload destination.

ts
import { parseFormData, FileUpload } from 'remix/form-data-parser'
import { createFsFileStorage } from 'remix/file-storage/fs'

let storage = createFsFileStorage({ directory: './uploads' })

export async function action({ request }: { request: Request }) {
  let formData = await parseFormData(request, {}, async (fileUpload) => {
    let key = `avatars/${Date.now()}-${fileUpload.name}`

    // Convert FileUpload to File and store
    let file = new File([await fileUpload.arrayBuffer()], fileUpload.name, {
      type: fileUpload.type,
    })
    await storage.set(key, file)

    return key
  })

  let avatarKey = formData.get('avatar') as string
  return Response.json({ avatarKey })
}

Step 5: Handle Multiple Files

HTML forms can include multiple file inputs or a single input with multiple. Use formData.getAll() to retrieve all values for a field.

ts
import { parseFormData, FileUpload } from 'remix/form-data-parser'
import { createFsFileStorage } from 'remix/file-storage/fs'

let storage = createFsFileStorage({ directory: './uploads' })

export async function action({ request }: { request: Request }) {
  let formData = await parseFormData(request, {}, async (fileUpload) => {
    let key = `gallery/${Date.now()}-${fileUpload.name}`
    let file = new File([await fileUpload.arrayBuffer()], fileUpload.name, {
      type: fileUpload.type,
    })
    await storage.set(key, file)
    return key
  })

  // Get all uploaded photo keys
  let photoKeys = formData.getAll('photos') as string[]
  let caption = formData.get('caption') as string

  return Response.json({ caption, photoKeys })
}

The corresponding HTML form:

html
<form method="post" enctype="multipart/form-data">
  <input name="caption" type="text" />
  <input name="photos" type="file" multiple accept="image/*" />
  <button type="submit">Upload Gallery</button>
</form>

Step 6: Validate Uploads

Validate file type and size inside the upload handler to reject bad files early.

ts
import { parseFormData, FileUpload } from 'remix/form-data-parser'

let ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp']
let MAX_SIZE = 5 * 1024 * 1024 // 5 MB

export async function action({ request }: { request: Request }) {
  try {
    let formData = await parseFormData(request, {}, async (fileUpload) => {
      if (!ALLOWED_TYPES.includes(fileUpload.type)) {
        throw new Response('File type not allowed. Use JPEG, PNG, or WebP.', {
          status: 415,
        })
      }

      if (fileUpload.size > MAX_SIZE) {
        throw new Response('File too large. Maximum size is 5 MB.', {
          status: 413,
        })
      }

      // Process the valid file...
      return fileUpload.name
    })

    return Response.json({ success: true })
  } catch (error) {
    if (error instanceof Response) return error
    throw error
  }
}

Summary

ConceptWhat You Learned
Basic parsingparseFormData(request) returns standard FormData
FileUploadFile fields arrive as FileUpload with name, type, size, stream()
Upload handlerAn async callback that receives each file and returns a replacement value
File storageCombine with file-storage for a backend-agnostic storage solution
Multiple filesUse formData.getAll('field') for multi-file inputs
ValidationCheck type and size inside the upload handler

Next Steps

Released under the MIT License.