Recipe

File Upload Patterns

Production-ready patterns for handling file uploads in Next.js App Router with Meridian.

Server Action Upload

// app/actions/upload.ts
'use server'

import { writeFile } from 'fs/promises'
import { join } from 'path'

export async function uploadFile(formData: FormData) {
  const file = formData.get('file') as File
  const bytes = await file.arrayBuffer()
  const buffer = Buffer.from(bytes)

  const path = join(process.cwd(), 'uploads', file.name)
  await writeFile(path, buffer)

  return { success: true, name: file.name }
}

Client Component

'use client'

import { uploadFile } from '@/actions/upload'

export function UploadForm() {
  return (
    <form action={uploadFile}>
      <input type="file" name="file"
        className="text-sm text-gray-300
          file:mr-4 file:py-2 file:px-4
          file:rounded file:border-0
          file:bg-[#8B5CF6] file:text-white
          hover:file:bg-[#F472B6]
          file:transition-colors" />
      <button type="submit"
        className="ml-4 px-4 py-2 bg-[#8B5CF6]
          rounded text-sm hover:bg-[#F472B6]
          transition-colors">
        Upload
      </button>
    </form>
  )
}

Validation & Limits

  • Check file.size before processing — reject payloads over your limit
  • Validate MIME type via file.type — never trust the extension alone
  • Sanitize filenames: strip path separators, limit length, reject null bytes
  • Stream large files to object storage (S3/R2) instead of buffering in memory

Edge Notes

Server Actions have a 4.5 MB body limit on Vercel. For larger files, use a direct upload endpoint with export const runtime = 'nodejs' or presigned URLs to bypass the edge runtime entirely.

Always set maxDuration in your route segment config when processing uploads on Hobby plans to avoid 10-second timeouts.