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
POSTrequests - 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.
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.
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.
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.
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.
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:
<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.
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
| Concept | What You Learned |
|---|---|
| Basic parsing | parseFormData(request) returns standard FormData |
| FileUpload | File fields arrive as FileUpload with name, type, size, stream() |
| Upload handler | An async callback that receives each file and returns a replacement value |
| File storage | Combine with file-storage for a backend-agnostic storage solution |
| Multiple files | Use formData.getAll('field') for multi-file inputs |
| Validation | Check type and size inside the upload handler |
Next Steps
- Store files in S3 with file-storage-s3
- Serve uploaded files with response helpers
- See the API Reference for all options