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
1000.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
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
Return
Returns a BlobObject.
get()
Returns a blob body.
const blob = await hubBlob().get(pathname)
Params
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
true, a random suffix will be added to the blob's name. Defaults to false.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
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
'files'.true, the formKey field will be an array of Blob objects.ensureBlob() options for more details.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
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
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
Return
Returns a BlobMultipartUpload
Params
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
'admin-read-write'.'admin-read-write' | 'admin-read-only' | 'object-read-write' | 'object-read-only'
900.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
maxSize or types should be provided.(
1 | 2 | 4 | 8 | 16 | 32 | 64 | 128 | 256 | 512 | 1024) + (B | KB | MB | GB) e.g.
'512KB', '1MB', '2GB', etc.['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
'files'.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
handleMultipartUpload().10MB.1.3.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
Settingstab 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 |