Storage

Blob

Store and upload images, videos and other unstructured data in your Nuxt application.
NuxtHub Blob is a layer on top of Cloudflare R2, allowing to store large amounts of unstructured data (images, videos, etc.).

Getting Started

Enable the blob storage in your NuxtHub project by adding the blob property to the hub object in your nuxt.config.ts file.

nuxt.config.ts
export default defineNuxtConfig({
  hub: {
    blob: true
  }
})

hubBlob()

Server composable that returns a set of methods to manipulate the blob storage.

list()

Returns a paginated list of blobs.

server/api/files.get.ts
export default eventHandler(async () => {
  const { blobs } = await hubBlob().list({ limit: 10 })

  return blobs
})

Params

options
Object

The list options.

Return

Returns BlobListResult.

serve()

Returns a blob's data.

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

  return hubBlob().serve(event, pathname)
})

Params

event
H3Event

Handler's event, needed to set headers.

pathname
String

The name of the blob to serve.

Return

Returns the blob's raw data and sets Content-Type and Content-Length headers.

Returns a blob's metadata.

const blob = await hubBlob().head(pathname)

Params

pathname
String

The name of the blob to serve.

Return

Returns a BlobObject.

put()

Uploads a blob to the storage.

server/api/files.post.ts
export default eventHandler(async (event) => {
  const form = await readFormData(event)
  const file = form.get('file') as File

  if (!file || !file.size) {
    throw createError({ statusCode: 400, message: 'No file provided' })
  }

  ensureBlob(file, {
    maxSize: '1MB',
    types: ['image']
  })

  return hubBlob().put(file.name, file, {
    addRandomSuffix: false,
    prefix 'images'
  })
})

See an example on the Vue side:

pages/upload.vue
<script setup lang="ts">
async function uploadImage (e: Event) {
  const form = e.target as HTMLFormElement

  await $fetch('/api/files', {
    method: 'POST',
    body: new FormData(form)
  }).catch((err) => alert('Failed to upload image:\n'+ err.data?.message))

  form.reset()
}
</script>

<template>
  <form @submit.prevent="uploadImage">
    <label>Upload an image: <input type="file" name="image"></label>
    <button type="submit">
      Upload
    </button>
  </form>
</template>

Params

pathname
String

The name of the blob to serve.

body
String | ReadableStream<any> | ArrayBuffer | ArrayBufferView | Blob

The blob's data.

options
Object

The put options. Any other provided field will be stored in the blob's metadata.

Return

Returns a BlobObject.

del()

Delete a blob with its pathname.

server/api/files/[...pathname].delete.ts
export default eventHandler(async (event) => {
  const { pathname } = getRouterParams(event)

  await hubBlob().del(pathname)

  return sendNoContent(event)
})

You can also delete multiple blobs at once by providing an array of pathnames:

await hubBlob().del(['images/1.jpg', 'images/2.jpg'])
You can also use the delete() method as alias of del().

Params

pathname
String

The name of the blob to serve.

Return

Returns nothing.

handleUpload()

This is an "all in one" function to validate a Blob by checking its size and type and upload it to the storage.

This server util is made to be used with the useUpload() Vue composable.

It can be used to handle file uploads in API routes.

export default eventHandler(async (event) => {
  return hubBlob().handleUpload(event, {
    formKey: 'files', // read file or files form the `formKey` field of request body (body should be a `FormData` object)
    multiple: true, // when `true`, the `formKey` field will be an array of `Blob` objects
    ensure: {
      contentType: ['image/jpeg', 'images/png'], // allowed types of the file
    },
    put: {
      addRandomSuffix: true
    }
  })
})

Params

formKey
string

The form key to read the file from. Defaults to 'files'.

multiple
boolean

When true, the formKey field will be an array of Blob objects.

ensure
BlobEnsureOptions

See ensureBlob() options for more details.

put
BlobPutOptions

See put() options for more details.

Return

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

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

handleMultipartUpload()

Handle the request to support multipart upload.

server/api/files/multipart/[action]/[...pathname].ts
export default eventHandler(async (event) => {
  return await hubBlob().handleMultipartUpload(event)
})
Make sure your route includes [action] and [...pathname] params.

On the client side, you can use the useMultipartUpload() composable to upload a file in parts.

<script setup lang="ts">
async function uploadFile(file: File) {
  const upload = useMultipartUpload('/api/files/multipart')

  const { progress, completed, abort } = upload(file)
}
</script>
See useMultipartUpload() on usage details.

Params

contentType
string

The content type of the blob.

contentLength
string

The content length of the blob.

addRandomSuffix
boolean

If true, a random suffix will be added to the blob's name. Defaults to false.

createMultipartUpload()

We suggest to use handleMultipartUpload() method to handle the multipart upload request.

Start a new multipart upload.

server/api/files/multipart/[...pathname].post.ts
export default eventHandler(async (event) => {
  const { pathname } = getRouterParams(event)

  const mpu = await hubBlob().createMultipartUpload(pathname)

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

Params

pathname
String

The name of the blob to serve.

options
Object

The put options. Any other provided field will be stored in the blob's metadata.

Return

Returns a BlobMultipartUpload

resumeMultipartUpload()

We suggest to use handleMultipartUpload() method to handle the multipart upload request.

Continue processing of unfinished multipart upload.

To upload a part of the multipart upload, you can use the uploadPart() method:

server/api/files/multipart/[...pathname].put.ts
export default eventHandler(async (event) => {
  const { pathname } = getRouterParams(event)
  const { uploadId, partNumber } = getQuery(event)

  const stream = getRequestWebStream(event)!
  const body = await streamToArrayBuffer(stream, contentLength)

  const mpu = hubBlob().resumeMultipartUpload(pathname, uploadId)
  return await mpu.uploadPart(partNumber, body)
})

Complete the upload by calling complete() method:

server/api/files/multipart/complete.post.ts
export default eventHandler(async (event) => {
  const { pathname, uploadId } = getQuery(event)
  const parts = await readBody(event)

  const mpu = hubBlob().resumeMultipartUpload(pathname, uploadId)
  return await mpu.complete(parts)
})

If you want to cancel the upload, you need to call abort() method:

server/api/files/multipart/[...pathname].delete.ts
export default eventHandler(async (event) => {
  const { pathname } = getRouterParams(event)
  const { uploadId } = getQuery(event)

  const mpu = hubBlob().resumeMultipartUpload(pathname, uploadId)
  await mpu.abort()

  return sendNoContent(event)
})

A simple example of multipart upload in client with above routes:

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

  const count = Math.ceil(file.size / chunkSize)
  const { pathname, uploadId } = await $fetch(
    `/api/files/multipart/${file.name}`,
    { method: 'POST' },
  )

  const uploaded = []

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

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

    uploaded.push(part)
  }

  return await $fetch(
    '/api/files/multipart/complete',
    {
      method: 'POST',
      query: { pathname, uploadId },
      body: { parts: uploaded },
    },
  )
}

Params

pathname
String

The name of the blob to serve.

uploadId
String

The upload ID of the multipart upload.

Return

Returns a BlobMultipartUpload

Params

eventrequired
H3Event

The event to handle.

ensureBlob()

ensureBlob() is a handy util to validate a Blob by checking its size and type:

// Will throw an error if the file is not an image or is larger than 1MB
ensureBlob(file, { maxSize: '1MB', types: ['image' ]})

Params

filerequired
Blob

The file to validate.

optionsrequired
Object

Note that at least maxSize or types should be provided.

Return

Returns nothing.

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

Composables

The following composables are meant to be used in the Vue side of your application (not the server/ directory).

useUpload()

useUpload is to handle file uploads in your Nuxt application.

<script setup lang="ts">
const upload = useUpload('/api/blob', { method: 'PUT' })

async function onFileSelect({ target }: Event) {
  const uploadedFiles = await upload(target as HTMLInputElement)

  // file uploaded successfully
}
</script>

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

Params

apiBaserequired
string

The base URL of the upload API.

optionsrequired
Object

Optionally, you can pass Fetch options to the request. Read more about Fetch API here.

Return

Return a MultipartUpload function that can be used to upload a file in parts.

const { completed, progress, abort } = upload(file)
const data = await completed

useMultipartUpload()

Application composable that creates a multipart upload helper.

utils/multipart-upload.ts
export const mpu = useMultipartUpload('/api/files/multipart')

Params

baseURL
string

The base URL of the multipart upload API handled by handleMultipartUpload().

options

The options for the multipart upload helper.

Return

Return a MultipartUpload function that can be used to upload a file in parts.

const { completed, progress, abort } = mpu(file)
const data = await completed

Types

BlobObject

interface BlobObject {
  pathname: string
  contentType: string | undefined
  size: number
  uploadedAt: Date,
  customMetadata?: Record<string, string> | undefined
}

BlobMultipartUpload

export interface 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

export interface BlobUploadedPart {
  partNumber: number;
  etag: string;
}

MultipartUploader

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

BlobListResult

interface BlobListResult {
  blobs: BlobObject[]
  hasMore: boolean
  cursor?: string
  folders?: string[]
}

Examples

List blobs with pagination

export default eventHandler(async (event) => {
  const { limit, cursor } = await getQuery(event)

  return hubBlob().list({
    limit: limit ? Number.parseInt(limit) : 10,
    cursor: cursor ? cursor : undefined
  })
})