Next.js
App Router adapter — one route handler on the server, one hook on the client.
Route handler
createUploadRoute returns a single POST handler that serves both the presign and complete steps. The client SDK distinguishes them by an action field in the request body — you don't need two routes.
app/api/upload/route.ts
// app/api/upload/route.ts
import { StorageClient } from 'sonuslab-storage/server'
import { createUploadRoute } from 'sonuslab-storage/react'
const storage = new StorageClient({
apiKey: process.env.SONUSLAB_STORAGE_API_KEY!,
})
export const { POST } = createUploadRoute({ storage })Client component
Both presignEndpoint and completeEndpoint point at the same route.
components/Uploader.tsx
'use client'
import { useUpload } from 'sonuslab-storage/react'
export function Uploader() {
const { upload, status, progress, error, reset } = useUpload({
presignEndpoint: '/api/upload',
completeEndpoint: '/api/upload',
})
return (
<div>
<input
type="file"
disabled={status === 'uploading'}
onChange={(e) => {
const file = e.target.files?.[0]
if (file) upload(file)
}}
/>
{status === 'uploading' && <p>{progress}%</p>}
{status === 'error' && (
<>
<p>{error?.message}</p>
<button onClick={reset}>Retry</button>
</>
)}
</div>
)
}Hook return shape
| Parameter | Type | Description |
|---|---|---|
upload | (file: File, metadata?: Record<string, unknown>) => Promise<UploadResult> | Start the upload. Resolves with fileId + key. |
status | 'idle' | 'uploading' | 'complete' | 'error' | Current state. |
progress | { loaded, total, percent } | null | Percent complete during the PUT. |
error | Error | null | Set when status is error. |
reset | () => void | Clear back to idle. |
Lifecycle callbacks
Pass these in useUpload({ ... }) for analytics or toasts without depending on useEffect.
| Parameter | Type | Description |
|---|---|---|
onStart | (file: File) => void | Fires when upload() is invoked, before any network I/O. |
onComplete | (result: UploadResult) => void | Fires after the complete step resolves. |
onError | (err: Error) => void | Fires when the upload rejects (after status becomes error). |
Auth
Use the onPresign hook to authenticate the user and inject metadata before the SDK mints the upload URL. Throwing a Response short-circuits the request.
app/api/upload/route.ts
export const { POST } = createUploadRoute({
storage,
onPresign: async (req, body) => {
const user = await getUserFromCookie(req)
if (!user) throw new Response('Unauthorized', { status: 401 })
return { metadata: { userId: user.id } }
},
})Server-only
sonuslab-storage/server must only be imported in route handlers, server actions, or other server-only files. The API key must never reach the client bundle.