# Build with Vercel Blob on Next.js

**Author:** Ben Sabic

---

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](https://vercel.com/templates/next.js/blob-starter) removes that setup by giving you a working image uploader built on [Vercel Blob](https://vercel.com/storage/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.

## Overview

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
  

## Prerequisites

- Node.js 22+ and a package manager (e.g., [**pnpm**](https://pnpm.io/))
  
- A [Vercel account](https://vercel.com/signup) with Vercel Blob
  
- Vercel CLI installed (`npm i -g vercel`)
  

## Deploy the template

Deploy the starter from the [template page](https://vercel.com/templates/next.js/blob-starter) by clicking **Deploy**. Vercel forks the [repository](https://github.com/vercel/examples/tree/main/storage/blob-starter) 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.

## Run the template locally

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

`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:

`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:

`pnpm dev`

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

## How 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.

## Walk through the code

### The 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:

`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.

### The 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:

`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. ### The 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: `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. ## Best practices ### Authenticate 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. ### Test 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. ### Choose 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. ## Related resources and next steps - Read the [Vercel Blob documentation](https://vercel.com/docs/vercel-blob) for access modes, caching, and pricing details
  
- Learn more about [client uploads](https://vercel.com/docs/vercel-blob/client-upload) and the `handleUpload()` helper
  
- Review [server uploads](https://vercel.com/docs/vercel-blob/server-upload) for files under 4.5 MB that can pass through your server
  
- Explore the [@vercel/blob SDK reference](https://vercel.com/docs/vercel-blob/using-blob-sdk) for `put()`, `get()`, `list()`, and `del()`
  
- Check the [Next.js App Router documentation](https://nextjs.org/docs/app) for more on Route Handlers

---

[View full KB sitemap](/kb/sitemap.md)
