SonusLab StorageSonusLab Storage

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)
ParameterTypeDescription
filerequiredFile | BlobThe file to upload.
presignEndpointrequiredstringURL of the presign route on your server.
completeEndpointrequiredstringURL of the complete route on your server.
metadataRecord<string, string>Forwarded to /presign as part of the body.
onProgress(p: { loaded, total, percent }) => voidFires during the PUT.
signalAbortSignalCancel 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.