> ## Documentation Index
> Fetch the complete documentation index at: https://upstash-dx-2982-blob-docs.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Quickstart

Upstash Blob is S3-compatible object storage. `@upstash/blob` has a `Bucket` client for your server, and an upload handler plus React hooks that upload from the browser straight to storage. This page builds a working file picker on Next.js App Router. Other frameworks work the same way; see [Other frameworks](/blob/uploads/upload-handler#other-frameworks).

***

## Setup

<CodeGroup>
  ```bash npm theme={"system"}
  npm install @upstash/blob
  ```

  ```bash pnpm theme={"system"}
  pnpm add @upstash/blob
  ```

  ```bash yarn theme={"system"}
  yarn add @upstash/blob
  ```

  ```bash bun theme={"system"}
  bun add @upstash/blob
  ```
</CodeGroup>

Create a bucket in the [Upstash Console](https://console.upstash.com) and put its token in your environment.

```bash .env theme={"system"}
UPSTASH_BLOB_TOKEN=...
```

The console asks whether the bucket is public or private:

* **Public**: every object has a public URL. For avatars, product images, anything a page links to directly.
* **Private**: no public URL. Every read goes through a time-limited [signed URL](/blob/bucket/reading#signedreadurl). For user documents, invoices, anything that must not be guessable.

***

## Upload from your server

```ts lib/reports.ts theme={"system"}
import { Bucket } from "@upstash/blob"

const bucket = Bucket.fromEnv()

export async function saveReport(file: Blob) {
  const blob = await bucket.put("reports/2026-01.pdf", file, {
    contentType: "application/pdf",
  })
  return blob.url
}
```

Bytes already on your server go to the bucket with `put`. `blob.url` is the public object URL, `undefined` on a private bucket. See [Writing](/blob/bucket/writing).

***

## Upload from the browser

Files a user picks go straight from the browser to storage. Your server only authorizes the upload and records the result, so the bytes never pass through it.

<Steps>
  <Step title="Write the upload handler">
    ```ts lib/uploads.ts theme={"system"}
    import "server-only"
    import { uploadHandler } from "@upstash/blob"

    export const uploads = uploadHandler({
      constraints: {
        maxSize: "20mb",
        contentTypes: ["image/*", "application/pdf"],
      },

      onBeforeUpload: ({ file }) => ({ path: `images/${file.name}` }),
    })
    ```

    The handler runs on your server. It decides who may upload, where the object goes, and what happens once it lands. It never sees the bytes.

    This one accepts anyone. [Upload handler](/blob/uploads/upload-handler) adds the auth check and the completion callback.
  </Step>

  <Step title="Mount it as a route">
    ```ts app/api/upload/route.ts theme={"system"}
    import { uploads } from "@/lib/uploads"

    export const { GET, POST } = uploads
    ```

    `POST` runs the upload and `GET` serves the route's constraints. The hooks look at `/api/upload` by default, so nothing else has to name a URL.
  </Step>

  <Step title="Bind the hooks to the handler">
    ```ts lib/upload-hooks.ts theme={"system"}
    "use client"

    import { uploadHooks } from "@upstash/blob/react"
    import type { uploads } from "./uploads"

    export const { useUpload } = uploadHooks<typeof uploads>()
    ```

    `uploadHooks<typeof uploads>()` reads the handler's type, so route names and completion data are checked at compile time. The `import type` is erased at build time and never pulls server code into the browser bundle.
  </Step>

  <Step title="Upload a file">
    ```tsx app/page.tsx theme={"system"}
    "use client"

    import { useUpload } from "@/lib/upload-hooks"

    export default function Page() {
      const { start, upload, accept } = useUpload()

      return (
        <div>
          <input
            type="file"
            accept={accept}
            onChange={(e) => start({ file: e.target.files?.[0] })}
          />

          {upload && <p>{upload.status}</p>}
          {upload?.pending && <progress value={upload.percent} max={100} />}
          {upload?.status === "done" && (
            <a href={upload.blob.url}>{upload.blob.path}</a>
          )}
          {upload?.status === "error" && <p>{upload.error.message}</p>}
        </div>
      )
    }
    ```
  </Step>
</Steps>

You get these without more code:

* **Multipart for large files.** Past 16 MB the SDK switches to parts, with pause, resume and per-part retry. See [Large files](/blob/uploads/large-files).
* **Retries.** Failed parts back off and retry, and an expired signature is refreshed mid-upload.
* **A picker that matches the server.** `accept` comes from the route's own `GET`, so an oversized file is refused before any request goes out. See [Constraints](/blob/uploads/constraints).
* **Progress.** `percent`, `status` and `pending` read the same for one PUT or 200 parts.
* **Types end to end.** Whatever `onUploadComplete` returns is `upload.blob.data` on the client.

***

## Next steps

<CardGroup cols={2}>
  <Card title="Upload handler" href="/blob/uploads/upload-handler">
    Auth, routes, and the completion callback in full.
  </Card>

  <Card title="Upload client" href="/blob/uploads/upload-client">
    `useUpload`, the record it renders, and the non-React client.
  </Card>

  <Card title="Writing" href="/blob/bucket/writing">
    `put`, metadata, conditional writes and multipart from the server.
  </Card>

  <Card title="Recipes" href="/blob/recipes/avatars">
    Avatars, attachments and private documents, wired end to end.
  </Card>
</CardGroup>
