Skip to main content
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

npm i remix

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

parseFormData

Parses form data from a request, handling file uploads via a custom upload handler.
function parseFormData(
  request: Request,
  uploadHandler: FileUploadHandler | ParseFormDataOptions
): Promise<FormData>
request
Request
required
The incoming request to parse.
uploadHandler
FileUploadHandler | ParseFormDataOptions
required
Function to handle file uploads, or options object containing the handler.
returns
Promise<FormData>
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
fileUpload
FileUpload
required
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

fieldName
string
The name of the form field (from <input name="...">).
filename
string
The original filename of the uploaded file.
mediaType
string
The MIME type of the file (e.g., 'image/jpeg').
size
number
The size of the file in bytes.
arrayBuffer
ArrayBuffer
The file data as an ArrayBuffer.
bytes
Uint8Array
The file data as a Uint8Array.
text
string
The file data decoded as UTF-8 text.

ParseFormDataOptions

uploadHandler
FileUploadHandler
required
Function to handle file uploads.
maxFileSize
number
Maximum file size in bytes. Throws MaxFileSizeExceededError if exceeded.Default: Infinity (no limit)
maxFiles
number
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

FormDataParseError

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

Native request.formData()

// ❌ 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

parseFormData from this package

// ✅ 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

Build docs developers (and LLMs) love