Client uploads with Vercel Blob
18 June 2025 (Updated 18 June 2025)
Setup
Install NPM package:
npm i @vercel/blob
Create blob store in the Vercel dashboard.

You should get a BLOB_READ_WRITE_TOKEN
value on creating a Blob database. Make note of this in a safe place like 1Password.
Set the BLOB_READ_WRITE_TOKEN
env variable in your .env
file:
BLOB_READ_WRITE_TOKEN=<your-token>
Create a client upload page
'use client'
import { type PutBlobResult } from '@vercel/blob'
import { upload } from '@vercel/blob/client'
import { useState, useRef } from 'react'
export default function AvatarUploadPage() {
const inputFileRef = useRef<HTMLInputElement>(null)
const [blob, setBlob] = useState<PutBlobResult | null>(null)
return (
<>
<h1>Upload Your Avatar</h1>
<form
onSubmit={async (event) => {
event.preventDefault();
if (!inputFileRef.current?.files) {
throw new Error('No file selected');
}
const file = inputFileRef.current.files[0];
const newBlob = await upload(file.name, file, {
access: 'public',
handleUploadUrl: '/api/avatar/upload',
});
setBlob(newBlob);
}}
>
<input name="file" ref={inputFileRef} type="file" required />
<button type="submit">Upload</button>
</form>
{blob && (
<div>
Blob url: <a href={blob.url}>{blob.url}</a>
</div>
)}
</>
)
}
Create a client upload route
import { handleUpload, type HandleUploadBody } from '@vercel/blob/client';
import { NextResponse } from 'next/server';
export async function POST(request: Request): Promise<NextResponse> {
const body = (await request.json()) as HandleUploadBody;
try {
const jsonResponse = await handleUpload({
body,
request,
onBeforeGenerateToken: async (
pathname,
/* clientPayload */
) => {
// Generate a client token for the browser to upload the file
// ⚠️ Authenticate and authorize users before generating the token.
// Otherwise, you're allowing anonymous uploads.
return {
allowedContentTypes: ['image/jpeg', 'image/png', 'image/webp'],
addRandomSuffix: true,
tokenPayload: JSON.stringify({
// optional, sent to your server on upload completion
// you could pass a user id from auth, or a value from clientPayload
}),
};
},
onUploadCompleted: async ({ blob, tokenPayload }) => {
// Get notified of client upload completion
// ⚠️ This will not work on `localhost` websites,
// Use ngrok or similar to get the full upload flow
console.log('blob upload completed', blob, tokenPayload);
try {
// Run any logic after the file upload completed
// const { userId } = JSON.parse(tokenPayload);
// await db.update({ avatar: blob.url, userId });
} catch (error) {
throw new Error('Could not update user');
}
},
});
return NextResponse.json(jsonResponse);
} catch (error) {
return NextResponse.json(
{ error: (error as Error).message },
{ status: 400 }, // The webhook will retry 5 times waiting for a 200
);
}
}
Configure next.config.ts
Add your blob’s host to your next.config.ts
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [
new URL('https://my-store-id.public.blob.vercel-storage.com/**'),
],
},
};
module.exports = nextConfig;
List Blobs
Now, you can list your blobs using the next/image
component:
import { list } from '@vercel/blob';
import Image from 'next/image';
export async function Images() {
const { blobs } = await list();
return (
<section>
{blobs.map((image, i) => (
<Image
priority={i < 2}
key={image.pathname}
src={image.url}
alt="My Image"
width={200}
height={200}
/>
))}
</section>
);
}
Tagged:
Next.js