Tutorial: Parse Form Submissions
In this tutorial you will parse text form submissions, handle file uploads, set size limits to protect your server, and write a custom upload handler to store files on disk.
Prerequisites
- A Remix project with a working server (see the Getting Started guide).
Step 1: Add the Form Data Middleware
import { createRouter } from 'remix/fetch-router'
import { formData, FormData } from 'remix/form-data-middleware'
import { route } from 'remix/fetch-router/routes'
let routes = route({
contactForm: '/contact',
submitContact: '/contact',
})
let router = createRouter({
middleware: [
formData(),
],
})Step 2: Build a Contact Form
Create a GET handler that renders the form:
router.get(routes.contactForm, () => {
return html`
<h1>Contact Us</h1>
<form method="post" action="/contact">
<label for="name">Name</label>
<input type="text" id="name" name="name" required />
<label for="email">Email</label>
<input type="email" id="email" name="email" required />
<label for="message">Message</label>
<textarea id="message" name="message" required></textarea>
<button type="submit">Send</button>
</form>
`
})Step 3: Handle the Submission
Use the FormData context key to access the parsed form data:
router.post(routes.submitContact, async ({ context }) => {
let data = context.get(FormData)
let name = data.get('name') // string | null
let email = data.get('email') // string | null
let message = data.get('message') // string | null
// Validate
if (!name || !email || !message) {
return new Response('All fields are required', { status: 400 })
}
await sendContactEmail({ name, email, message })
return redirect('/contact/thanks')
})The FormData import from 'remix/form-data-middleware' is a context key, not the global FormData constructor. Use context.get(FormData) to retrieve the parsed data.
Step 4: Handle File Uploads
To accept file uploads, add enctype="multipart/form-data" to your form:
router.get(routes.uploadForm, () => {
return html`
<h1>Upload Avatar</h1>
<form method="post" action="/profile/avatar" enctype="multipart/form-data">
<label for="avatar">Choose an image</label>
<input type="file" id="avatar" name="avatar" accept="image/*" />
<button type="submit">Upload</button>
</form>
`
})Access the uploaded file in your handler:
router.post(routes.uploadAvatar, async ({ context }) => {
let data = context.get(FormData)
let avatar = data.get('avatar')
if (avatar instanceof File) {
console.log('Filename:', avatar.name)
console.log('Size:', avatar.size, 'bytes')
console.log('Type:', avatar.type)
// Read the file contents
let bytes = await avatar.arrayBuffer()
// ... store it somewhere
}
return redirect('/profile')
})Step 5: Set Size Limits
Protect your server from oversized uploads:
formData({
maxFileSize: 5 * 1024 * 1024, // 5 MB per file
maxFiles: 3, // Maximum 3 files per request
maxParts: 50, // Maximum 50 total fields
})If a file exceeds maxFileSize, the middleware returns a 413 Payload Too Large response before your handler runs.
Step 6: Write a Custom Upload Handler
The default handler stores files in memory, which is fine for small files. For larger files, write a custom handler that streams to disk:
import { writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import type { UploadHandler } from 'remix/form-data-middleware'
let diskUploadHandler: UploadHandler = async ({ filename, contentType, stream }) => {
let safeName = `${crypto.randomUUID()}-${filename}`
let path = join('./uploads', safeName)
// Collect the stream into a buffer and write to disk
let chunks: Uint8Array[] = []
let reader = stream.getReader()
while (true) {
let { done, value } = await reader.read()
if (done) break
chunks.push(value)
}
let buffer = Buffer.concat(chunks)
await writeFile(path, buffer)
return new File([buffer], filename, { type: contentType })
}
formData({
uploadHandler: diskUploadHandler,
maxFileSize: 50 * 1024 * 1024, // 50 MB
})The upload handler receives a stream (a ReadableStream of the file contents) along with the filename and contentType. It returns a File object that your handler can work with.
Step 7: Handle Multiple File Uploads
Use getAll() to retrieve multiple files from the same field:
<input type="file" name="photos" multiple />router.post(routes.uploadPhotos, async ({ context }) => {
let data = context.get(FormData)
let photos = data.getAll('photos') // Array of File objects
for (let photo of photos) {
if (photo instanceof File) {
console.log(`Uploaded: ${photo.name} (${photo.size} bytes)`)
}
}
return redirect('/gallery')
})Step 8: Combine with Other Middleware
The formData middleware works well with CSRF protection and method override:
let router = createRouter({
middleware: [
session(sessionCookie, sessionStorage),
csrf(),
formData(),
methodOverride(),
],
})The order matters:
session()-- makes the session available for CSRF.csrf()-- validates the CSRF token from form data.formData()-- parses the form body.methodOverride()-- reads_methodfrom parsed form data.
What You Learned
- How to parse text form submissions using the
FormDatacontext key. - How to handle file uploads with
enctype="multipart/form-data". - How to set size limits to protect against oversized uploads.
- How to write a custom upload handler for disk storage.
- How to handle multiple file uploads from a single field.
Next Steps
- API Reference -- Full details on options, upload handlers, and the
FormDatacontext key. - method-override-middleware -- Use form data with RESTful method override.
- csrf-middleware -- Protect forms from cross-site request forgery.