File Uploads & Storage
Image uploads and storage in the Nuxt template.
The template includes an authenticated image-upload Example and a reusable
uploader UI. The endpoint writes to local public/uploads; replace that storage
boundary before relying on uploads in production.
Included flow
POST /api/storage/upload-image is implemented by
server/api/storage/upload-image.post.ts:
requireAuth(event)rejects unauthenticated requests.readMultipartFormDataselects parts namedfiles.- Each file must declare an
image/*MIME type and be no larger than 5 MB. - The handler generates a
crypto.randomUUID()filename and writes it underpublic/uploads. - It returns
{ urls: string[] }, using an absolute URL whenruntimeConfig.public.baseUrlexists and a relative/uploads/...URL otherwise.
app/components/ai/ImageUploader.vue posts FormData to that route, maintains
preview/upload/error state, and emits uploaded URLs to its parent.
The current handler trusts the multipart MIME declaration; it does not inspect file signatures. Add magic-byte/decoder validation before treating this endpoint as hardened. Also note that the uploader UI defaults to a 10 MB client limit while the server enforces 5 MB—align these values in your product.
Why local storage is not production storage
public/uploads works for local development and a single persistent server. It
is not durable on Vercel/serverless functions or replaceable containers, and it
is not shared across multiple instances. A deployment or restart can make files
disappear.
Because files are written beneath the public directory, every returned URL is publicly readable. Authentication protects creation, not subsequent reads.
Replace the storage boundary
Keep multipart parsing, authentication, validation, and response shape in the
route, but replace the local mkdir/writeFile block with an object-storage
adapter:
const object = await storage.put({
key: `uploads/${crypto.randomUUID()}.${extension}`,
body: file.data,
contentType: detectedMime,
})
urls.push(object.publicUrl)S3, Cloudflare R2, and Vercel Blob all fit this boundary. For private content, store object keys rather than public URLs and return short-lived signed URLs from an authorized read endpoint.
Production requirements
- Content validation: detect signatures (JPEG/PNG/GIF/WebP), decode images, reject mismatches, and strip risky metadata where appropriate.
- Limits: enforce per-file size, file count, image dimensions, and total user quota on the server. Never rely on the browser limit.
- Authorization: decide who may upload, replace, read, and delete each object; associate ownership in the database.
- Safe keys: keep generated keys and an explicit extension allowlist; never write a user-supplied path.
- Lifecycle: delete abandoned objects, handle replacements, and define data retention/backups.
- Delivery: configure CDN/cache headers and the correct public base URL or signed URL policy.
- Abuse controls: add rate limits, malware scanning where relevant, logging, and cost alerts.
Verify
- An unauthenticated upload returns 401.
- A valid image below 5 MB returns a usable URL.
- Missing
files, non-image MIME, and oversized input return 4xx responses. - Spoofed MIME content is rejected after adding signature validation.
- Files remain available after deployment/restart and across instances after moving to object storage.
- Delete and retention behavior matches the product policy.
