File Uploads

Learn how to upload files in your Nuxt application using validation, simple uploads, and multipart uploads for large files.

This page covers different methods to upload files to your Blob Storage, from simple file uploads to large file multipart uploads.

Simple Upload

For most use cases, you can use handleUpload() on the server and useUpload() on the client for a streamlined file upload experience.

This example create an API route to handle file uploads on /api/upload. It validates the files to be images of less than 10MB, and uploads them to Blob Storage with a random suffix and the images prefix.

server/api/upload.post.ts
import { blob } from 'hub:blob'

export default eventHandler(async (event) => {
  // Make sure to check if the user can upload files before calling this function
  return blob.handleUpload(event, {
    formKey: 'files',
    multiple: true,
    ensure: {
      maxSize: '10MB',
      types: ['image/jpeg', 'image/png', 'image/webp'],
    },
    put: {
      addRandomSuffix: true,
      prefix: 'images',
    },
  })
})

handleUpload()

A server function to handle file uploads. It validates the files and uploads them to Blob Storage.

await blob.handleUpload(event, options)

Params

event
H3Event required
The event handler's event.
options
Object
The upload options.

Return

Returns a BlobObject or an array of BlobObject if multiple is true.

Throws an error if the file doesn't meet the requirements.

useUpload()

A Vue composable to handle file uploads on the client.

pages/upload.vue
<script setup lang="ts">
const upload = useUpload('/api/upload')

async function onFileSelect(event: Event) {
  const target = event.target as HTMLInputElement
  const uploadedFiles = await upload(target)
  // Files uploaded successfully
  console.log('Uploaded:', uploadedFiles)
}
</script>

<template>
  <input
    accept="image/jpeg, image/png, image/webp"
    type="file"
    name="files"
    multiple
    @change="onFileSelect"
  >
</template>

Params

apiBase
string required
The base URL of the upload API endpoint.
options
Object
Fetch options for the upload request.

Return

Returns a function that accepts an HTMLInputElement and returns a Promise resolving to the uploaded blob data.

Full Example

import { blob } from 'hub:blob'

export default eventHandler(async (event) => {
  return blob.handleUpload(event, {
    formKey: 'files',
    multiple: true,
    ensure: {
      maxSize: '10MB',
      types: ['image'],
    },
    put: {
      addRandomSuffix: true,
      prefix: 'avatars',
    },
  })
})

Multipart Upload

For large files (typically > 10MB), use multipart uploads to split the file into smaller chunks. This provides better reliability and allows you to track upload progress.

handleMultipartUpload()

A server function to handle multipart upload requests. It handles creating, uploading parts, completing, and aborting multipart uploads.

server/api/files/multipart/[action]/[...pathname].ts
import { blob } from 'hub:blob'

export default eventHandler(async (event) => {
  return blob.handleMultipartUpload(event)
})
Make sure your route includes [action] and [...pathname] params. The action param handles the different multipart operations (create, upload, complete, abort).

Params

event
H3Event required
The event handler's event.
options
Object
The multipart upload options.

Return

Returns a response based on the action:

  • create: Returns { pathname, uploadId }
  • upload: Returns { partNumber, etag }
  • complete: Returns a BlobObject
  • abort: Returns nothing

useMultipartUpload()

A Vue composable to handle multipart file uploads on the client with progress tracking.

pages/upload-large.vue
<script setup lang="ts">
const mpu = useMultipartUpload('/api/files/multipart')

const file = ref<File | null>(null)
const progress = ref(0)
const uploading = ref(false)

async function uploadFile() {
  if (!file.value) return

  uploading.value = true
  const { completed, progress: uploadProgress, abort } = mpu(file.value)

  // Track progress
  watch(uploadProgress, (value) => {
    progress.value = value
  })

  try {
    const blob = await completed
    console.log('Upload complete:', blob)
  } catch (error) {
    console.error('Upload failed:', error)
  } finally {
    uploading.value = false
  }
}
</script>

<template>
  <div>
    <input type="file" @change="(e) => file = e.target.files?.[0]">
    <button @click="uploadFile" :disabled="!file || uploading">
      Upload
    </button>
    <div v-if="uploading">
      Progress: {{ Math.round(progress * 100) }}%
    </div>
  </div>
</template>
Multipart uploads are supported on Cloudflare R2, S3, Vercel Blob, and filesystem drivers.

Params

baseURL
string required
The base URL of the multipart upload API handled by handleMultipartUpload().
options
Object
The options for the multipart upload helper.

Return

Returns a function that accepts a File and returns:

{
  completed: Promise<BlobObject | undefined>  // Resolves when upload completes
  progress: Readonly<Ref<number>>             // Upload progress (0 to 1)
  abort: () => Promise<void>                  // Function to cancel the upload
}
When using the Vercel Blob driver, useMultipartUpload() automatically uses the Vercel Blob Client SDK for uploads.

Full Example

import { blob } from 'hub:blob'

export default eventHandler(async (event) => {
  return blob.handleMultipartUpload(event, {
    addRandomSuffix: true,
    prefix: 'uploads',
  })
})

Advanced Multipart

For more control over the multipart upload process, you can use the low-level APIs directly.

We recommend using handleMultipartUpload() and useMultipartUpload() for most use cases. Use these low-level APIs only when you need custom behavior.

createMultipartUpload()

Start a new multipart upload.

server/api/files/multipart/[...pathname].post.ts
import { blob } from 'hub:blob'

export default eventHandler(async (event) => {
  const { pathname } = getRouterParams(event)

  const mpu = await blob.createMultipartUpload(pathname, {
    addRandomSuffix: true,
  })

  return {
    uploadId: mpu.uploadId,
    pathname: mpu.pathname,
  }
})

Params

pathname
string required
The pathname for the blob.
options
Object

Return

Returns a BlobMultipartUpload object.

resumeMultipartUpload()

Resume an existing multipart upload to upload parts, complete, or abort it.

Upload a part:

server/api/files/multipart/[...pathname].put.ts
import { blob } from 'hub:blob'

export default eventHandler(async (event) => {
  const { pathname } = getRouterParams(event)
  const { uploadId, partNumber } = getQuery(event)
  const body = await readRawBody(event)

  const mpu = blob.resumeMultipartUpload(pathname, uploadId as string)
  return mpu.uploadPart(Number(partNumber), body)
})

Complete the upload:

server/api/files/multipart/complete.post.ts
import { blob } from 'hub:blob'

export default eventHandler(async (event) => {
  const { pathname, uploadId } = getQuery(event)
  const parts = await readBody(event)

  const mpu = blob.resumeMultipartUpload(pathname as string, uploadId as string)
  return mpu.complete(parts)
})

Abort the upload:

server/api/files/multipart/[...pathname].delete.ts
import { blob } from 'hub:blob'

export default eventHandler(async (event) => {
  const { pathname } = getRouterParams(event)
  const { uploadId } = getQuery(event)

  const mpu = blob.resumeMultipartUpload(pathname, uploadId as string)
  await mpu.abort()

  return sendNoContent(event)
})

Params

pathname
string required
The pathname of the multipart upload.
uploadId
string required
The upload ID from createMultipartUpload().

Return

Returns a BlobMultipartUpload object with methods:

  • uploadPart(partNumber, body): Upload a single part
  • complete(parts): Complete the upload with all uploaded parts
  • abort(): Cancel the upload

Client Implementation Example

app/utils/multipart-upload.ts
async function uploadLargeFile(file: File) {
  const chunkSize = 10 * 1024 * 1024 // 10MB per part

  // 1. Create the multipart upload
  const { pathname, uploadId } = await $fetch(`/api/files/multipart/${file.name}`, {
    method: 'POST',
  })

  // 2. Upload parts
  const totalParts = Math.ceil(file.size / chunkSize)
  const uploadedParts = []

  for (let i = 0; i < totalParts; i++) {
    const start = i * chunkSize
    const end = Math.min(start + chunkSize, file.size)
    const chunk = file.slice(start, end)
    const partNumber = i + 1

    const part = await $fetch(`/api/files/multipart/${pathname}`, {
      method: 'PUT',
      query: { uploadId, partNumber },
      body: chunk,
    })

    uploadedParts.push(part)
    console.log(`Uploaded part ${partNumber}/${totalParts}`)
  }

  // 3. Complete the upload
  const blob = await $fetch('/api/files/multipart/complete', {
    method: 'POST',
    query: { pathname, uploadId },
    body: uploadedParts,
  })

  return blob
}

Types

BlobObject

{
  pathname: string
  contentType: string | undefined
  size: number
  httpEtag: string | undefined
  uploadedAt: Date
  httpMetadata: Record<string, string>
  customMetadata: Record<string, string>
  url: string | undefined
}

BlobMultipartUpload

{
  pathname: string
  uploadId: string
  uploadPart(
    partNumber: number,
    value: string | ReadableStream<any> | ArrayBuffer | ArrayBufferView | Blob
  ): Promise<BlobUploadedPart>
  abort(): Promise<void>
  complete(uploadedParts: BlobUploadedPart[]): Promise<BlobObject>
}

BlobUploadedPart

{
  partNumber: number
  etag: string
}

MultipartUploader

(file: File) => {
  completed: Promise<BlobObject | undefined>
  progress: Readonly<Ref<number>>
  abort: () => Promise<void>
}