Blob Storage
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.
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 (metadata only).
export default eventHandler(async () => {
const { blobs } = await hubBlob().list({ limit: 10 })
return blobs
})
Params
The list options.
The maximum number of blobs to return per request. Defaults to 1000
.
Filters the results to only those that begin with the specified prefix.
The cursor to continue from a previous list operation.
If true
, the list will be folded using /
separator and list of folders will be returned.
Return
Returns BlobListResult
.
Return all blobs
To fetch all blobs, you can use a while
loop to fetch the next page until the cursor
is null
.
let blobs = []
let cursor = null
do {
const res = await hubBlob().list({ cursor })
blobs.push(...res.blobs)
cursor = res.cursor
} while (cursor)
serve()
Returns a blob's data and sets Content-Type
, Content-Length
and ETag
headers.
export default eventHandler(async (event) => {
const { pathname } = getRouterParams(event)
return hubBlob().serve(event, pathname)
})
<template>
<img src="/images/my-image.jpg">
</template>
You can also set a Content-Security-Policy
header to add an additional layer of security:
export default eventHandler(async (event) => {
const { pathname } = getRouterParams(event)
setHeader(event, 'Content-Security-Policy', 'default-src \'none\';')
return hubBlob().serve(event, pathname)
})
Params
Handler's event, needed to set headers.
The name of the blob to serve.
Return
Returns the blob's raw data and sets Content-Type
and Content-Length
headers.
head()
Returns a blob's metadata.
const metadata = await hubBlob().head(pathname)
Params
The name of the blob to serve.
Return
Returns a BlobObject
.
get()
Returns a blob body.
const blob = await hubBlob().get(pathname)
Params
The name of the blob to serve.
Return
Returns a Blob
or null
if not found.
put()
Uploads a blob to the storage.
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:
<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
The name of the blob to serve.
The blob's data.
The put options. Any other provided field will be stored in the blob's metadata.
The content type of the blob. If not given, it will be inferred from the Blob or the file extension.
The content length of the blob.
If true
, a random suffix will be added to the blob's name. Defaults to false
.
The prefix to use for the blob pathname.
An object with custom metadata to store with the blob.
Return
Returns a BlobObject
.
del()
Delete a blob with its pathname.
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'])
delete()
method as alias of del()
.Params
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.
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: {
types: ['image/jpeg', 'image/png'], // allowed types of the file
},
put: {
addRandomSuffix: true
}
})
})
<script setup lang="ts">
const upload = useUpload('/api/blob', { method: 'PUT' })
async function onFileSelect(event: Event) {
const uploadedFiles = await upload(event.target as HTMLInputElement)
// file uploaded successfully
}
</script>
<template>
<input type="file" name="file" @change="onFileSelect" multiple accept="image/jpeg, image/png" />
</template>
Params
The form key to read the file from. Defaults to 'files'
.
When true
, the formKey
field will be an array of Blob
objects.
See ensureBlob()
options for more details.
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.
export default eventHandler(async (event) => {
return await hubBlob().handleMultipartUpload(event)
})
[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>
useMultipartUpload()
on usage details.Params
The content type of the blob.
The content length of the blob.
If true
, a random suffix will be added to the blob's name. Defaults to false
.
createMultipartUpload()
handleMultipartUpload()
method to handle the multipart upload request.Start a new multipart upload.
export default eventHandler(async (event) => {
const { pathname } = getRouterParams(event)
const mpu = await hubBlob().createMultipartUpload(pathname)
return {
uploadId: mpu.uploadId,
pathname: mpu.pathname,
}
})
Params
The name of the blob to serve.
The put options. Any other provided field will be stored in the blob's metadata.
The content type of the blob. If not given, it will be inferred from the Blob or the file extension.
The content length of the blob.
If true
, a random suffix will be added to the blob's name. Defaults to true
.
Return
Returns a BlobMultipartUpload
resumeMultipartUpload()
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:
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:
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:
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:
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
The name of the blob to serve.
The upload ID of the multipart upload.
Return
Returns a BlobMultipartUpload
Params
The event to handle.
createCredentials()
Creates temporary access credentials that can be optionally scoped to prefixes or objects.
Useful to create presigned URLs to upload files to R2 from client-side (see example).
--remote
flag.// Create credentials with default permission & scope (admin-read-write)
const credentials = await hubBlob().createCredentials()
// Limit the scope to a specific object & permission
const credentials = await hubBlob().createCredentials({
permission: 'object-read-write',
pathnames: ['only-this-file.png']
})
Read more about creating presigned URLs to upload files to R2.
Params
The options to create the credentials.
The permission of the credentials, defaults to 'admin-read-write'
.
'admin-read-write' | 'admin-read-only' | 'object-read-write' | 'object-read-only'
The ttl of the credentials in seconds, defaults to 900
.
The pathnames to scope the credentials to.
The prefixes to scope the credentials to.
Return
Returns an object with the following properties:
{
accountId: string
bucketName: string
accessKeyId: string
secretAccessKey: string
sessionToken: string
}
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
The file to validate.
Note that at least maxSize
or types
should be provided.
The maximum size of the file, should be:
(1
| 2
| 4
| 8
| 16
| 32
| 64
| 128
| 256
| 512
| 1024
) + (B
| KB
| MB
| GB
)
e.g. '512KB'
, '1MB'
, '2GB'
, etc.
Allowed types of the file, e.g. ['image/jpeg']
.
Return
Returns nothing.
Throws an error if file
doesn't meet the requirements.
Vue Composables
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="image/jpeg, image/png"
type="file"
name="file"
multiple
@change="onFileSelect"
>
</template>
Params
The base URL of the upload API.
Optionally, you can pass Fetch options to the request. Read more about Fetch API here.
The key to add the file/files to the request form. Defaults to 'files'
.
Whether to allow multiple files to be uploaded. Defaults to true
.
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.
export const mpu = useMultipartUpload('/api/files/multipart')
Params
The base URL of the multipart upload API handled by handleMultipartUpload()
.
The options for the multipart upload helper.
The size of each part of the file to be uploaded. Defaults to 10MB
.
The maximum number of concurrent uploads. Defaults to 1
.
The maximum number of retry attempts for the whole upload. Defaults to 3
.
The prefix to use for the blob pathname.
Override the ofetch options.
The query
and headers
will be merged with the options provided by the uploader.
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
httpEtag: string
uploadedAt: Date
httpMetadata: Record<string, string>
customMetadata: Record<string, string>
}
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
})
})
<script setup lang="ts">
const blobs = ref<BlobObject[]>([])
const hasMore = ref(true)
const cursor = ref()
async function loadMore() {
if (!hasMore.value) {
return
}
const res = await $fetch('/api/blobs', {
query: {
limit: 3,
cursor: cursor.value
}
})
blobs.value.push(...res.blobs)
hasMore.value = res.hasMore
cursor.value = res.cursor
}
</script>
<template>
<div>
<div v-for="blob in blobs" :key="blob.pathname">
{{ blob.pathname }}
</div>
<button @click="loadMore">
Load more
</button>
</div>
</template>
Create presigned URLs to upload files to R2
Presigned URLs can be used to upload files to R2 from client-side without using an API key.

As we use aws4fetch to sign the request and zod to validate the request, we need to install the packages:
npx nypm i aws4fetch zod
First, we need to create an API route that will return a presigned URL to the client.
import { z } from 'zod'
import { AwsClient } from 'aws4fetch'
export default eventHandler(async (event) => {
const { pathname } = await getValidatedRouterParams(event, z.object({
pathname: z.string().min(1)
}).parse)
// Create credentials with the right permission & scope
const blob = hubBlob()
const { accountId, bucketName, ...credentials } = await blob.createCredentials({
permission: 'object-read-write',
pathnames: [pathname]
})
// Create the presigned URL
const client = new AwsClient(credentials)
const endpoint = new URL(
pathname,
`https://${bucketName}.${accountId}.r2.cloudflarestorage.com`
)
const { url } = await client.sign(endpoint, {
method: 'PUT',
aws: { signQuery: true }
})
// Return the presigned URL to the client
return { url }
})
Next, we need to create the Vue page to upload a file to our R2 bucket using the presigned URL:
<script setup lang="ts">
async function uploadWithPresignedUrl(file: File) {
const { url } = await $fetch(`/api/blob/sign/${file.name}`)
await $fetch(url, {
method: 'PUT',
body: file
})
}
</script>
<template>
<input type="file" @change="uploadWithPresignedUrl($event.target.files[0])">
</template>
At this stage, you will get a CORS error because we did not setup the CORS on our R2 bucket.
To setup the CORS on our R2 bucket:
- Open the project on NuxtHub Admin with
npx nuxthub manage
- Go to the Blob tab (make sure to be on the right environment: production or preview)
- Click on the Cloudflare icon on the top right corner
- Once on Cloudflare, Go to the
Settings
tab of your R2 bucket - Scroll to CORS policy
- Click on
Edit CORS policy
- Update the allowed origins with your origins by following this example:
[
{
"AllowedOrigins": [
"http://localhost:3000",
"https://my-app.nuxt.dev"
],
"AllowedMethods": [
"GET",
"PUT"
],
"AllowedHeaders": [
"*"
]
}
]
- Save the changes
That's it! You can now upload files to R2 using the presigned URLs.
Pricing
Free Tier | Pricing | |
---|---|---|
List + Write | 1 million / month | 1 million / month + $4.50/million |
Read | 10 million / month | 10 million / month + $0.36/million |
Storage | 10 GB / month | 10 GB / month + $0.015/GB-month |
Egress | Unlimited | Unlimited |