Blob Storage Lab
Upload, list, and delete objects with Vercel Blob - one-click store provisioning on Deploy.
Run this experiment yourself
Demos are not embedded on this site. Deploy a standalone copy on Vercel or run the experiment app locally.
Local development
cd apps/experiments/blob-storage-lab pnpm install pnpm dev
Then open http://localhost:3010.
This is an experimental demo. Use it as a starting point for your own projects.
The Blob Storage Lab shows the full lifecycle of a small public object on Vercel Blob: upload text from the browser, list recent uploads, open the CDN URL, and delete it. A single Route Handler (app/api/blob/upload/route.ts) wraps the @vercel/blob SDK (put, list, del), validates the input with Zod, and degrades to an honest 503 setup message when no Blob store is connected.
Features
- Upload from the browser. The demo turns the textarea contents into a
File(text/plain) and sends it asmultipart/form-data. - List recent uploads. Shows up to 20 objects under the
experiments/blob-lab/prefix, with pathname, size, and upload time. - Delete with a guard. Only URLs containing
/experiments/blob-lab/can be deleted, so the demo cannot remove unrelated objects in the same store. - Hard limits. Uploads are capped at 256 KiB (
262144bytes) and filenames are restricted to[a-zA-Z0-9._-], 1–80 characters. - Honest missing-config state. Without
BLOB_READ_WRITE_TOKEN, every method returns503with asetuphint and the UI shows a warning instead of failing silently. - One-click provisioning. The Deploy button passes a Blob store (
access: "public") so the token is injected for you.
API Reference
All methods live at a single path and share the same availability check. If BLOB_READ_WRITE_TOKEN is unset, every method returns 503.
GET /api/blob/upload
Lists up to 20 blobs whose pathname starts with experiments/blob-lab/.
Prop
Type
POST /api/blob/upload
Uploads one file with multipart/form-data. The stored pathname is experiments/blob-lab/<Date.now()>-<filename> with access: "public" and addRandomSuffix: false.
Prop
Type
DELETE /api/blob/upload
Deletes one blob by URL. Send a JSON body.
Prop
Type
Implementation Details
Detect the service before touching the SDK
getServiceStatus('blob') in lib/vercel/services.ts reports Blob as available only when BLOB_READ_WRITE_TOKEN is set. Each handler checks it first and returns 503 otherwise.
const status = getServiceStatus('blob');
if (!status.available) {
return NextResponse.json(
{
error: 'Vercel Blob is not configured',
setup: 'Add BLOB_READ_WRITE_TOKEN or deploy with the Blob store in one click.',
},
{ status: 503 },
);
}Validate size and filename
The size cap is a constant, and the filename is validated with Zod before any write.
const MAX_BYTES = 256 * 1024;
const metaSchema = z.object({
filename: z
.string()
.min(1)
.max(80)
.regex(/^[a-zA-Z0-9._-]+$/),
});apps/experiments/blob-storage-lab/logic.ts exports the same limit (MAX_UPLOAD_BYTES) and a matching isAllowedFilename() so the client can fail fast.
Write with put() under a fixed prefix
All uploads go under experiments/blob-lab/ with a timestamp so names do not collide.
const pathname = `experiments/blob-lab/${Date.now()}-${parsed.data.filename}`;
const blob = await put(pathname, file, {
access: 'public',
addRandomSuffix: false,
});List and delete within the prefix
const { blobs } = await list({ prefix: 'experiments/blob-lab/', limit: 20 });if (!url.data.url.includes('/experiments/blob-lab/')) {
return NextResponse.json({ error: 'Can only delete blob-lab uploads' }, { status: 403 });
}
await del(url.data.url);Drive it from the client
demo.tsx loads the list on mount, treats a 503 as "not configured", and builds the upload from the textarea.
const file = new File([text], filename, { type: 'text/plain' });
const form = new FormData();
form.set('file', file);
form.set('filename', filename);
const res = await fetch('/api/blob/upload', { method: 'POST', body: form });Use Cases
- Avatars, attachments, and user-generated assets that should be served from a CDN URL.
- Build artifacts, exports, and generated files (reports, PDFs, JSON snapshots) produced by Route Handlers or cron jobs.
- A reference for guarded storage routes: fixed prefix, size cap, filename allow-list, and prefix-checked delete.
- A starting point before moving to client uploads for larger files.
Limitations
- Small files only. The route rejects anything over 256 KiB with
413. Files go through your Route Handler, so this is not a large-file upload pattern. - Public access. Objects are written with
access: "public", so anyone with the URL can read them. Do not upload secrets or personal data. - No authentication. The demo route has no user check. Anyone who can reach the deployment can upload and delete lab files.
- Prefix check is a substring match. Delete authorization is
url.includes('/experiments/blob-lab/'), which is enough for a demo but is not a strict ownership check. - List is capped.
GETreturns at most 20 items and does not paginate. - SDK errors are not caught. If the token is present but invalid, or the Blob service errors, the exception from
put,list, ordelis not handled by the route.
Use in your project
Minimal public upload route. Install the SDK with pnpm add @vercel/blob and provide BLOB_READ_WRITE_TOKEN.
// app/api/upload/route.ts
import { put } from '@vercel/blob';
import { NextResponse } from 'next/server';
const MAX_BYTES = 256 * 1024;
export async function POST(request: Request) {
const form = await request.formData();
const file = form.get('file');
if (!(file instanceof File)) {
return NextResponse.json({ error: 'file is required' }, { status: 400 });
}
if (file.size > MAX_BYTES) {
return NextResponse.json({ error: `Max upload size is ${MAX_BYTES} bytes` }, { status: 413 });
}
const blob = await put(`uploads/${Date.now()}-${file.name}`, file, {
access: 'public',
addRandomSuffix: false,
});
return NextResponse.json({ url: blob.url, pathname: blob.pathname, size: file.size });
}Add authentication and your own filename policy before using this in production.
Deployment
Use the one-click Deploy button below. It clones the repository and provisions a public Vercel Blob store, which injects BLOB_READ_WRITE_TOKEN into the new project.
The site-wide platform Deploy button also includes a Blob store, along with Upstash Redis and Neon.
Local Development
pnpm install
pnpm devWithout a token the lab runs but shows the 503 warning. To use a real store, link the project and pull its variables (or paste the token manually):
vercel env pull .env.local
# or
echo 'BLOB_READ_WRITE_TOKEN=vercel_blob_rw_...' >> .env.localTry the API directly (default port 3000):
# List
curl http://localhost:3000/api/blob/upload
# Upload
curl -X POST http://localhost:3000/api/blob/upload \
-F "file=@./hello.txt;type=text/plain" \
-F "filename=hello.txt"
# Delete (use a url returned by the upload or list call)
curl -X DELETE http://localhost:3000/api/blob/upload \
-H "content-type: application/json" \
-d '{"url":"https://<store>.public.blob.vercel-storage.com/experiments/blob-lab/<file>"}'Expected response when no token is set:
{
"error": "Vercel Blob is not configured",
"setup": "Add BLOB_READ_WRITE_TOKEN or deploy with the Blob store in one click."
}Configuration
From .env.example:
| Variable | Required | Purpose |
|---|---|---|
BLOB_READ_WRITE_TOKEN | Yes, for live uploads | Read/write token for the Vercel Blob store. Injected automatically when the store is provisioned. |
Constants in app/api/blob/upload/route.ts: MAX_BYTES = 256 * 1024, prefix experiments/blob-lab/, list limit: 20.
Vercel / Next.js Features Used
- Vercel Blob (
@vercel/blob):put,list, anddel. - Route Handlers with
GET,POST, andDELETEexports andrequest.formData(). - Deploy Button
storesparameter (BLOB_STOREinlib/vercel/deploy.ts) for one-click Blob provisioning. - Zod for filename and DELETE body validation.