Browser Uploads
Upload bytes from the browser via a short-lived upload URL — without ever exposing your API key.
Never ship the API key to the browser
The API key grants full access to your storage app. Always proxy uploads through your server: one route to mint the upload URL, one to finalize. The key never leaves your backend.
Server routes
Two Nitro handlers — the bare minimum. Add auth/rate-limit/validation as your app needs.
server/api/upload/presign.post.ts
import { StorageClient } from 'sonuslab-storage/server'
const storage = new StorageClient({
apiKey: process.env.SONUSLAB_STORAGE_API_KEY!,
})
export default defineEventHandler(async (event) => {
// TODO: authenticate the user, validate body shape
const body = await readBody<{ name: string; size: number; contentType: string }>(event)
return storage.presign(body)
})server/api/upload/complete.post.ts
export default defineEventHandler(async (event) => {
const { fileId } = await readBody<{ fileId: string }>(event)
const file = await storage.completeUpload({ fileId })
// TODO: persist file.id against the user/resource
return file
})Browser: high-level helper
uploadFile handles presign → PUT → complete in one call.
import { uploadFile } from 'sonuslab-storage/client'
const result = await uploadFile({
file,
presignEndpoint: '/api/upload/presign',
completeEndpoint: '/api/upload/complete',
metadata: { userId: 'abc' },
onProgress: (p) => console.log(`${p.percent}% (${p.loaded}/${p.total})`),
signal: abortController.signal,
})
console.log(result.fileId, result.key)| Parameter | Type | Description |
|---|---|---|
filerequired | File | Blob | The file to upload. |
presignEndpointrequired | string | URL of the presign route on your server. |
completeEndpointrequired | string | URL of the complete route on your server. |
metadata | Record<string, string> | Forwarded to /presign as part of the body. |
onProgress | (p: { loaded, total, percent }) => void | Fires during the PUT. |
signal | AbortSignal | Cancel the upload mid-flight. |
fetch | (typeof fetch)? | Custom fetch implementation. |
Lower-level: uploadToPresigned
If you've already minted a presign response (e.g. from a different transport), use uploadToPresigned to run just the PUT step.
import { uploadToPresigned } from 'sonuslab-storage/client'
await uploadToPresigned({
presignResponse,
file,
onProgress: (p) => console.log(p.percent),
signal: controller.signal,
})Progress and cancellation
<script setup lang='ts'>
import { ref } from 'vue'
import { uploadFile } from 'sonuslab-storage/client'
const percent = ref(0)
const controller = ref<AbortController | null>(null)
async function start(file: File) {
controller.value = new AbortController()
try {
await uploadFile({
file,
presignEndpoint: '/api/upload/presign',
completeEndpoint: '/api/upload/complete',
onProgress: (p) => { percent.value = p.percent },
signal: controller.value.signal,
})
} catch (err) {
if ((err as Error).name === 'AbortError') console.log('cancelled')
else throw err
}
}
function cancel() {
controller.value?.abort()
}
</script> Each app has a maxFileSize set on creation; oversize requests are rejected with a 413 before bytes leave the browser. For larger files, use multipart uploads.