Skip to content
Docs

Build with Vercel Blob on Next.js

Deploy the Vercel Blob Next.js Starter and learn how client uploads store images securely in a private Blob store.

Ben SabicContent Engineer

Handling user file uploads usually means provisioning a storage bucket, wiring up credentials, and writing upload logic before you can ship anything. The Vercel Blob Next.js Starter removes that setup by giving you a working image uploader built on Vercel Blob, Vercel's object storage service.

The template uses client uploads, so files up to 50 MB go directly from the browser to your private Blob store without passing through your server. This guide walks through deploying the template, running it locally, and understanding how each part of the code works so you can adapt it to your own application.

Copy link to headingOverview

In this guide, you'll learn how to:

  • Deploy the Blob starter template with a Blob store provisioned automatically
  • Clone and run the template locally with the right environment variables
  • Understand how client uploads secure the browser-to-store transfer
  • Serve private blobs through a delivery route that you control

Copy link to headingPrerequisites

  • Node.js 22+ and a package manager (e.g., pnpm)
  • A Vercel account with Vercel Blob
  • Vercel CLI installed (npm i -g vercel)

Copy link to headingDeploy the template

Deploy the starter from the template page by clicking Deploy. Vercel forks the repository to your Git account, creates a project, provisions a Blob store, and configures the environment variables for you.

When the deployment finishes, open your project's URL and upload an image. Drag a file onto the upload area or click to select one, then select Upload. A progress bar tracks the transfer, and a toast notification links to the uploaded file when it completes.

You can inspect the uploaded blob from your dashboard. Go to your project, open the Storage tab, and select the Blob store to browse files, view metadata, and delete blobs.

Copy link to headingRun the template locally

If you'd rather start from the code, bootstrap the example with create-next-app:

Terminal
pnpm create next-app --example https://github.com/vercel/examples/tree/main/storage/blob-starter

The app needs a BLOB_READ_WRITE_TOKEN environment variable to generate client upload tokens. If you deployed the template first, link your local folder to the Vercel project and pull the variables:

Terminal
vercel link
vercel env pull

If you haven't deployed yet, create a Blob store from your dashboard under Storage, then copy .env.example to .env.local and paste the token from the store's settings to .env.local.

Start the development server:

Terminal
pnpm dev

The app runs at http://localhost:3000, and uploads from your local machine go to the same Blob store as production.

Copy link to headingHow client uploads work

The template uses client uploads because they bypass the 4.5 MB request body limit that applies when a file passes through a Vercel Function. The file travels directly from the browser to the Blob store, and the template caps uploads at 50 MB.

Direct browser uploads still need authorization, which happens through a token exchange with your server. The flow has three parts:

  1. The browser calls upload() from @vercel/blob/client, which first requests a short-lived client token from your /api/upload route.
  2. Your server route validates the request with handleUpload() and returns a token that constrains what the browser can upload.
  3. The browser uploads the file directly to the Blob store using that token.

Because the store uses private access mode, uploaded files aren't reachable at their blob URL from a browser. The template includes a /api/blob delivery route that fetches and streams private blobs on request, which gives you a place to enforce your own access rules.

Copy link to headingWalk through the code

Copy link to headingThe upload component

The uploader in components/uploader.tsx is a client component. It validates that the file is an image under 50 MB, shows a local preview, then calls upload() on submit:

components/uploader.tsx
import { upload } from '@vercel/blob/client'
const blob = await upload(file.name, file, {
access: 'private',
handleUploadUrl: '/api/upload',
onUploadProgress: (progressEvent) => {
setProgress(progressEvent.percentage)
},
})

The handleUploadUrl option points at the server route that issues the client token. The onUploadProgress callback fires as parts of the file transfer, which drives the progress bar in the UI. After the upload resolves, the component builds a link to the delivery route using the returned blob.pathname rather than the raw blob URL, since private blobs can't be opened directly.

Copy link to headingThe token route

The route handler in app/api/upload/route.ts uses handleUpload() from @vercel/blob/client to generate client tokens and receive completion callbacks:

app/api/upload/route.ts
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 () => {
// Authenticate and authorize users before generating the token.
// Otherwise, you're allowing anonymous uploads.
return {
allowedContentTypes: ['image/*'],
addRandomSuffix: true,
maximumSizeInBytes: 50 * 1024 * 1024, // 50 MB
}
},
onUploadCompleted: async ({ blob, tokenPayload }) => {
console.log('blob upload completed', blob, tokenPayload)
},
})
return NextResponse.json(jsonResponse)
} catch (error) {
return NextResponse.json({ error: (error as Error).message }, { status: 400 })
}
}

The options returned from onBeforeGenerateToken are enforced server-side, so the browser can only upload images up to 50 MB even if someone bypasses the client-side checks. Setting addRandomSuffix: true appends a unique suffix to each pathname, which prevents collisions when two users upload files with the same name.

The onUploadCompleted callback is where you'd persist the blob URL to your database. Vercel calls it as a webhook after the upload finishes, and retries up to 5 times if your route doesn't return a 200 response.

Copy link to headingThe delivery route

The route handler in app/api/blob/route.ts serves private blobs to the browser. It reads a pathname query parameter, fetches the blob server-side with get(), and streams the content back:

app/api/blob/route.ts
import { type NextRequest, NextResponse } from 'next/server'
import { get } from '@vercel/blob'
export async function GET(request: NextRequest) {
// Add your own authentication here.
const pathname = request.nextUrl.searchParams.get('pathname')
if (!pathname) {
return NextResponse.json({ error: 'Missing pathname' }, { status: 400 })
}
const result = await get(pathname, { access: 'private' })
if (result?.statusCode !== 200) {
return new NextResponse('Not found', { status: 404 })
}
return new NextResponse(result.stream, {
headers: {
'Content-Type': result.blob.contentType,
'X-Content-Type-Options': 'nosniff',
},
})
}

This pattern puts your application in the request path for every download, which is what makes private storage useful. Check the user's session before calling get() and you have per-user access control over stored files.

Copy link to headingBest practices

Copy link to headingAuthenticate before generating tokens

The template's onBeforeGenerateToken callback returns a token to any caller, which means anyone who finds your deployment can upload files. Before shipping to production, check the user's session inside the callback and throw an error for unauthenticated requests. You can also store the user's ID in tokenPayload so onUploadCompleted knows who uploaded the file.

Copy link to headingTest upload callbacks with a tunnel

The onUploadCompleted callback won't fire during local development because Vercel can't reach localhost. To test it, expose your dev server with a tunneling tool such as ngrok, or verify the callback behavior on a preview deployment.

Copy link to headingChoose the access mode deliberately

The template uses a private store, where every download goes through your delivery route. If you're storing content that anyone may view, such as public avatars or marketing images, a public store serves files directly from the CDN with browser caching and no function invocation per download. You can't change a store's access mode after creating it, so decide based on your use case up front.

Copy link to headingRelated resources and next steps

Related documentation

More Vercel Blob guides