A streaming multipart/form-data parser that solves memory issues with file uploads in server environments. Built as an enhanced replacement for the native request.formData() API, it enables efficient handling of large file uploads by streaming directly to disk or cloud storage.
Installation
Features
- Drop-in Replacement for
request.formData() with streaming file upload support
- Minimal Buffering - Processes file upload streams with minimal memory footprint
- Standards-Based - Built on Web Streams API and File API
- Smart Fallback - Automatically uses native
request.formData() for non-multipart requests
- Storage Agnostic - Works with any storage backend (local disk, S3, R2, etc.)
- File Size Limiting - Built-in limits to prevent abuse
Why You Need This
The native request.formData() method has major flaws in server environments:
- Buffers all file uploads in memory - Can exhaust RAM and crash the application
- No fine-grained control - No way to stream files to storage as they arrive
- DoS attack vector - Malicious actors can overwhelm memory with large payloads
form-data-parser solves this by handling file uploads as they arrive in the request body stream, allowing you to safely store files and use either the File directly or a unique identifier in the returned FormData object.
Basic Usage
import * as fsp from 'node:fs/promises'
import type { FileUpload } from 'remix/form-data-parser'
import { parseFormData } from 'remix/form-data-parser'
// Define how to handle incoming file uploads
async function uploadHandler(fileUpload: FileUpload) {
// Check the field name
if (fileUpload.fieldName === 'user-avatar') {
let filename = `/uploads/user-${user.id}-avatar.bin`
// Store the file on disk
await fsp.writeFile(filename, fileUpload.bytes)
// Return the filename to use in FormData
return filename
}
// Ignore unrecognized fields
}
// Handle form submissions
async function requestHandler(request: Request) {
// Parse form data, passing files through upload handler
let formData = await parseFormData(request, uploadHandler)
let avatarFilename = formData.get('user-avatar')
if (avatarFilename != null) {
console.log(`User avatar uploaded to ${avatarFilename}`)
}
}
API Reference
Parses form data from a request, handling file uploads via a custom upload handler.
function parseFormData(
request: Request,
uploadHandler: FileUploadHandler | ParseFormDataOptions
): Promise<FormData>
The incoming request to parse.
uploadHandler
FileUploadHandler | ParseFormDataOptions
required
Function to handle file uploads, or options object containing the handler.
A FormData object with all form fields and processed file uploads.
FileUploadHandler
Function that processes file uploads and returns a value to include in FormData.
type FileUploadHandler = (
fileUpload: FileUpload
) => Promise<File | string | null | undefined> | File | string | null | undefined
The file upload to process.
returns
File | string | null | undefined
File: Include the File in FormData
string: Include the string in FormData (e.g., file path, storage key)
null or undefined: Skip this file (not included in FormData)
FileUpload
Represents an uploaded file.
Properties
The name of the form field (from <input name="...">).
The original filename of the uploaded file.
The MIME type of the file (e.g., 'image/jpeg').
The size of the file in bytes.
The file data as an ArrayBuffer.
The file data as a Uint8Array.
The file data decoded as UTF-8 text.
uploadHandler
FileUploadHandler
required
Function to handle file uploads.
Maximum file size in bytes. Throws MaxFileSizeExceededError if exceeded.Default: Infinity (no limit)
Maximum number of files allowed in the request. Throws MaxFilesExceededError if exceeded.Default: Infinity (no limit)
Examples
Basic File Upload
import * as fsp from 'node:fs/promises'
import { parseFormData } from 'remix/form-data-parser'
async function uploadHandler(upload) {
let filepath = `./uploads/${upload.filename}`
await fsp.writeFile(filepath, upload.bytes)
return filepath
}
let formData = await parseFormData(request, uploadHandler)
let filePath = formData.get('file')
console.log(`File saved to: ${filePath}`)
Limiting File Size
import {
MaxFileSizeExceededError,
parseFormData,
} from 'remix/form-data-parser'
const oneKb = 1024
const oneMb = 1024 * oneKb
try {
let formData = await parseFormData(request, {
maxFileSize: 10 * oneMb,
uploadHandler: async (upload) => {
// Process file
await saveFile(upload.filename, upload.bytes)
return upload.filename
},
})
} catch (error) {
if (error instanceof MaxFileSizeExceededError) {
return new Response('File too large (max 10 MB)', { status: 413 })
}
throw error
}
Limiting Number of Files
import {
MaxFilesExceededError,
parseFormData,
} from 'remix/form-data-parser'
try {
let formData = await parseFormData(request, {
maxFiles: 5,
uploadHandler: async (upload) => {
return await processFile(upload)
},
})
} catch (error) {
if (error instanceof MaxFilesExceededError) {
return new Response('Too many files (max 5)', { status: 400 })
}
throw error
}
Filtering by Field Name
import { parseFormData } from 'remix/form-data-parser'
async function uploadHandler(upload) {
// Only process avatar uploads
if (upload.fieldName === 'avatar') {
let key = `avatars/${userId}.jpg`
await storage.put(key, upload.bytes)
return key
}
// Ignore other fields
return null
}
let formData = await parseFormData(request, uploadHandler)
Filtering by File Type
async function uploadHandler(upload) {
// Only accept images
if (!upload.mediaType.startsWith('image/')) {
throw new Error('Only images are allowed')
}
await saveImage(upload.filename, upload.bytes)
return upload.filename
}
try {
let formData = await parseFormData(request, uploadHandler)
} catch (error) {
return new Response('Only images are allowed', { status: 400 })
}
Using with File Storage
Pairs perfectly with the file-storage package:
import { LocalFileStorage } from 'remix/file-storage/local'
import { parseFormData } from 'remix/form-data-parser'
// Set up storage
const fileStorage = new LocalFileStorage('/uploads/user-avatars')
// Upload handler
async function uploadHandler(upload) {
if (upload.fieldName === 'user-avatar') {
let storageKey = `user-${user.id}-avatar`
// Put the file in storage
await fileStorage.set(storageKey, upload)
// Return a lazy File that can access the stored file when needed
return fileStorage.get(storageKey)
}
}
let formData = await parseFormData(request, uploadHandler)
let avatarFile = formData.get('user-avatar')
if (avatarFile instanceof File) {
console.log(`Avatar: ${avatarFile.name}, ${avatarFile.size} bytes`)
}
Cloud Storage (S3)
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'
import { parseFormData } from 'remix/form-data-parser'
const s3 = new S3Client({})
async function uploadHandler(upload) {
let key = `uploads/${Date.now()}-${upload.filename}`
await s3.send(
new PutObjectCommand({
Bucket: 'my-bucket',
Key: key,
Body: upload.bytes,
ContentType: upload.mediaType,
})
)
return key
}
let formData = await parseFormData(request, uploadHandler)
let s3Key = formData.get('file')
console.log(`Uploaded to S3: ${s3Key}`)
Multiple File Fields
async function uploadHandler(upload) {
switch (upload.fieldName) {
case 'avatar':
return await saveAvatar(upload)
case 'banner':
return await saveBanner(upload)
case 'documents':
return await saveDocument(upload)
default:
return null // Ignore unknown fields
}
}
let formData = await parseFormData(request, uploadHandler)
let avatar = formData.get('avatar')
let banner = formData.get('banner')
let documents = formData.getAll('documents') // Multiple files
Returning File Objects
You can return File objects to include them in FormData:
async function uploadHandler(upload) {
// Process and return a File
let processed = await processImage(upload.bytes)
return new File(
[processed],
upload.filename,
{ type: upload.mediaType }
)
}
let formData = await parseFormData(request, uploadHandler)
let file = formData.get('image')
if (file instanceof File) {
console.log(`Processed: ${file.name}`)
}
Error Handling
Base error class for form data parsing errors.
class FormDataParseError extends Error {}
MaxFilesExceededError
Thrown when the number of files exceeds maxFiles.
class MaxFilesExceededError extends FormDataParseError {}
MaxFileSizeExceededError
Thrown when a file exceeds maxFileSize (re-exported from multipart-parser).
class MaxFileSizeExceededError extends MultipartParseError {}
Complete Error Handling
import {
FormDataParseError,
MaxFilesExceededError,
MaxFileSizeExceededError,
parseFormData,
} from 'remix/form-data-parser'
try {
let formData = await parseFormData(request, {
maxFiles: 5,
maxFileSize: 10 * 1024 * 1024, // 10 MB
uploadHandler: async (upload) => {
return await processUpload(upload)
},
})
} catch (error) {
if (error instanceof MaxFileSizeExceededError) {
return new Response('File too large', { status: 413 })
} else if (error instanceof MaxFilesExceededError) {
return new Response('Too many files', { status: 400 })
} else if (error instanceof FormDataParseError) {
return new Response('Invalid form data', { status: 400 })
}
throw error
}
Type Definitions
type FileUploadHandler = (
fileUpload: FileUpload
) => Promise<File | string | null | undefined> | File | string | null | undefined
interface ParseFormDataOptions {
uploadHandler: FileUploadHandler
maxFileSize?: number
maxFiles?: number
}
class FileUpload {
readonly fieldName: string
readonly filename: string
readonly mediaType: string
readonly size: number
readonly arrayBuffer: ArrayBuffer
readonly bytes: Uint8Array
readonly text: string
}
class FormDataParseError extends Error {}
class MaxFilesExceededError extends FormDataParseError {}
// Re-exported from multipart-parser
class MultipartParseError extends Error {}
class MaxFileSizeExceededError extends MultipartParseError {}
class MaxHeaderSizeExceededError extends MultipartParseError {}
function parseFormData(
request: Request,
uploadHandler: FileUploadHandler | ParseFormDataOptions
): Promise<FormData>
Best Practices
Always Set Limits
// ✅ Good: Set reasonable limits
let formData = await parseFormData(request, {
maxFileSize: 50 * 1024 * 1024, // 50 MB
maxFiles: 10,
uploadHandler,
})
// ❌ Bad: No limits (security risk)
let formData = await parseFormData(request, uploadHandler)
Validate File Types
// ✅ Good: Validate media types
async function uploadHandler(upload) {
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp']
if (!allowedTypes.includes(upload.mediaType)) {
throw new Error('Invalid file type')
}
return await saveFile(upload)
}
// ❌ Bad: Accept any file type
async function uploadHandler(upload) {
return await saveFile(upload)
}
Use Descriptive Field Names
// ✅ Good: Clear field name checking
if (upload.fieldName === 'profile-picture') {
return await saveProfilePicture(upload)
}
// ❌ Bad: Generic field names
if (upload.fieldName === 'file') {
// Which file?
}
Return Appropriate Values
// ✅ Good: Return storage reference
async function uploadHandler(upload) {
let key = await storage.put(upload.filename, upload.bytes)
return key // String identifier
}
// ❌ Bad: Return entire buffer
async function uploadHandler(upload) {
return upload.bytes // Defeats the purpose of streaming
}
Comparison with Native API
// ❌ Problems:
// - Buffers all files in memory
// - No size limits
// - No streaming
// - Can crash server
let formData = await request.formData()
let file = formData.get('file') // Entire file in memory
// ✅ Benefits:
// - Streams files as they arrive
// - Size limits built-in
// - Control over storage
// - Memory efficient
let formData = await parseFormData(request, {
maxFileSize: 10 * 1024 * 1024,
uploadHandler: async (upload) => {
// Stream to storage immediately
return await storage.put(upload.filename, upload.bytes)
},
})
let storageKey = formData.get('file') // Just the key