# Quickstart (/docs/quickstart)



You can have file uploads in your React app in a few minutes with Better Upload. This guide will walk you through the steps to set it up with any React framework.

Before you start, make sure you have an S3-compatible bucket ready. You can use AWS S3, Cloudflare R2, or any other S3-compatible service.

<PageSelect
  pages={[
  {
    href: '/docs/quickstart',
    title: 'Multiple files',
    description: 'Upload more than one file at once.',
  },
  {
    href: '/docs/quickstart-single',
    title: 'Single files',
    description: 'Upload only a single file at once.',
  },
]}
/>

## Uploading images

<Steps>
  <Step>
    ### Install

    Install the `@better-upload/server` and `@better-upload/client` packages.

    <CodeBlockTabs defaultValue="npm" groupId="package-manager" persist>
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="npm">
          npm
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="pnpm">
          pnpm
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="yarn">
          yarn
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="bun">
          bun
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="npm">
        ```bash
        npm i @better-upload/server @better-upload/client
        ```
      </CodeBlockTab>

      <CodeBlockTab value="pnpm">
        ```bash
        pnpm add @better-upload/server @better-upload/client
        ```
      </CodeBlockTab>

      <CodeBlockTab value="yarn">
        ```bash
        yarn add @better-upload/server @better-upload/client
        ```
      </CodeBlockTab>

      <CodeBlockTab value="bun">
        ```bash
        bun add @better-upload/server @better-upload/client
        ```
      </CodeBlockTab>
    </CodeBlockTabs>
  </Step>

  <Step>
    ### Set up server

    Your server generates pre-signed URLs, which the client uses to upload files directly to the S3 bucket.

    Change `my-bucket` to your bucket name, and [choose your S3 client](/docs/helpers-server#s3-clients).

        <CodeBlockTabs defaultValue="Next.js">
          <CodeBlockTabsList>
            <CodeBlockTabsTrigger value="Next.js">
              Next.js
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="TanStack Start">
              TanStack Start
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="Remix">
              Remix
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="Hono">
              Hono
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="Elysia">
              Elysia
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="Express">
              Express
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="Fastify">
              Fastify
            </CodeBlockTabsTrigger>
          </CodeBlockTabsList>

          <CodeBlockTab value="Next.js">
            ```ts  title="app/api/upload/route.ts"
            import { route, type Router } from '@better-upload/server';
            import { toRouteHandler } from '@better-upload/server/adapters/next';
            import { aws } from '@better-upload/server/clients';

            const router: Router = {
              client: aws(), // or cloudflare(), backblaze(), tigris(), ... // [!code highlight]
              bucketName: 'my-bucket', // [!code highlight]
              routes: {
                images: route({
                  fileTypes: ['image/*'],
                  multipleFiles: true,
                  maxFiles: 4,
                }),
              },
            };

            export const { POST } = toRouteHandler(router);
            ```
          </CodeBlockTab>

          <CodeBlockTab value="TanStack Start">
            ```ts  title="routes/api/upload.ts"
            import { createFileRoute } from '@tanstack/react-router';
            import { handleRequest, route, type Router } from '@better-upload/server';
            import { aws } from '@better-upload/server/clients';

            const router: Router = {
              client: aws(), // or cloudflare(), backblaze(), tigris(), ... // [!code highlight]
              bucketName: 'my-bucket', // [!code highlight]
              routes: {
                images: route({
                  fileTypes: ['image/*'],
                  multipleFiles: true,
                  maxFiles: 4,
                }),
              },
            };

            export const Route = createFileRoute('/api/upload')({
              server: {
                handlers: {
                  POST: async ({ request }) => {
                    return handleRequest(request, router);
                  },
                },
              },
            });
            ```
          </CodeBlockTab>

          <CodeBlockTab value="Remix">
            ```ts  title="app/routes/api.upload.ts"
            import { ActionFunctionArgs } from '@remix-run/node';
            import { handleRequest, route, type Router } from '@better-upload/server';
            import { aws } from '@better-upload/server/clients';

            const router: Router = {
              client: aws(), // or cloudflare(), backblaze(), tigris(), ... // [!code highlight]
              bucketName: 'my-bucket', // [!code highlight]
              routes: {
                images: route({
                  fileTypes: ['image/*'],
                  multipleFiles: true,
                  maxFiles: 4,
                }),
              },
            };

            export async function action({ request }: ActionFunctionArgs) {
              return handleRequest(request, router);
            }
            ```
          </CodeBlockTab>

          <CodeBlockTab value="Hono">
            ```ts
            // when using a separate backend server, make sure to update the `api` option on the client hooks.

            import { Hono } from 'hono';
            import { handleRequest, route, type Router } from '@better-upload/server';
            import { aws } from '@better-upload/server/clients';

            const router: Router = {
              client: aws(), // or cloudflare(), backblaze(), tigris(), ... // [!code highlight]
              bucketName: 'my-bucket', // [!code highlight]
              routes: {
                images: route({
                  fileTypes: ['image/*'],
                  multipleFiles: true,
                  maxFiles: 4,
                }),
              },
            };

            const app = new Hono();

            app.post('/upload', (c) => {
              return handleRequest(c.req.raw, router);
            });

            export default app;
            ```
          </CodeBlockTab>

          <CodeBlockTab value="Elysia">
            ```ts
            // when using a separate backend server, make sure to update the `api` option on the client hooks.

            import { Elysia } from 'elysia';
            import { handleRequest, route, type Router } from '@better-upload/server';
            import { aws } from '@better-upload/server/clients';

            const router: Router = {
              client: aws(), // or cloudflare(), backblaze(), tigris(), ... // [!code highlight]
              bucketName: 'my-bucket', // [!code highlight]
              routes: {
                images: route({
                  fileTypes: ['image/*'],
                  multipleFiles: true,
                  maxFiles: 4,
                }),
              },
            };

            const app = new Elysia()
              .post('/upload', ({ request }) => {
                return handleRequest(request, router);
              })
              .listen(3000);
            ```
          </CodeBlockTab>

          <CodeBlockTab value="Express">
            ```ts
            // when using a separate backend server, make sure to update the `api` option on the client hooks.

            import express from 'express';
            import { route, type Router } from '@better-upload/server';
            import { toNodeHandler } from '@better-upload/server/adapters/node';
            import { aws } from '@better-upload/server/clients';

            const router: Router = {
              client: aws(), // or cloudflare(), backblaze(), tigris(), ... // [!code highlight]
              bucketName: 'my-bucket', // [!code highlight]
              routes: {
                images: route({
                  fileTypes: ['image/*'],
                  multipleFiles: true,
                  maxFiles: 4,
                }),
              },
            };

            const app = express();

            app.post('/upload', toNodeHandler(router));

            // only mount express json middleware AFTER upload router
            app.use(express.json());

            app.listen(3000);
            ```
          </CodeBlockTab>

          <CodeBlockTab value="Fastify">
            ```ts
            // when using a separate backend server, make sure to update the `api` option on the client hooks.

            import Fastify from 'fastify';
            import { route, type Router } from '@better-upload/server';
            import { toNodeHandler } from '@better-upload/server/adapters/node';
            import { aws } from '@better-upload/server/clients';

            const router: Router = {
              client: aws(), // or cloudflare(), backblaze(), tigris(), ... // [!code highlight]
              bucketName: 'my-bucket', // [!code highlight]
              routes: {
                images: route({
                  fileTypes: ['image/*'],
                  multipleFiles: true,
                  maxFiles: 4,
                }),
              },
            };

            const app = Fastify();

            app.post('/upload', toNodeHandler(router));

            app.listen({ port: 3000 });
            ```
          </CodeBlockTab>
        </CodeBlockTabs>

    In the example above, we create the upload route `images`. Learn more about upload routes [here](/docs/routes-multiple).

        <Accordions>
          <Accordion title="Adding authentication">
            You can run code before uploads in the server. Use the `onBeforeUpload` callback:

            ```ts
            import { RejectUpload, route, type Router } from '@better-upload/server';
            import { aws } from '@better-upload/server/clients';

            const auth = (req: Request) => ({ id: 'fake-user-id' }); // [!code highlight]

            const router: Router = {
              client: aws(),
              bucketName: 'my-bucket',
              routes: {
                images: route({
                  fileTypes: ['image/*'],
                  multipleFiles: true,
                  maxFiles: 4,
                  // [!code ++:7]
                  onBeforeUpload: async ({ req, files, clientMetadata }) => {
                    const user = await auth(req);

                    if (!user) {
                      throw new RejectUpload('Not logged in!');
                    }
                  },
                }),
              },
            };
            ```
          </Accordion>

          <Accordion title="Modifying S3 object info">
            You can modify the S3 object through the `onBeforeUpload` callback:

            ```ts
            const router: Router = {
              client: aws(),
              bucketName: 'my-bucket',
              routes: {
                images: route({
                  fileTypes: ['image/*'],
                  multipleFiles: true,
                  maxFiles: 4,
                  // [!code ++:10]
                  onBeforeUpload: async ({ req, files, clientMetadata }) => {
                    return {
                      generateObjectInfo: ({ file }) => ({
                        key: `files/${file.name}`,
                        metadata: {
                          author: 'user_123',
                        },
                      }),
                    };
                  },
                }),
              },
            };
            ```
          </Accordion>

          <Accordion title="Available S3 clients">
            There are built-in clients for popular S3-compatible services. Suggest a new client by [opening an issue](https://github.com/Nic13Gamer/better-upload/issues).

                    <CodeBlockTabs defaultValue="AWS S3">
                      <CodeBlockTabsList>
                        <CodeBlockTabsTrigger value="AWS S3">
                          AWS S3
                        </CodeBlockTabsTrigger>

                        <CodeBlockTabsTrigger value="Cloudflare R2">
                          Cloudflare R2
                        </CodeBlockTabsTrigger>

                        <CodeBlockTabsTrigger value="Tigris">
                          Tigris
                        </CodeBlockTabsTrigger>

                        <CodeBlockTabsTrigger value="Backblaze B2">
                          Backblaze B2
                        </CodeBlockTabsTrigger>

                        <CodeBlockTabsTrigger value="DigitalOcean Spaces">
                          DigitalOcean Spaces
                        </CodeBlockTabsTrigger>

                        <CodeBlockTabsTrigger value="Wasabi">
                          Wasabi
                        </CodeBlockTabsTrigger>

                        <CodeBlockTabsTrigger value="MinIO">
                          MinIO
                        </CodeBlockTabsTrigger>

                        <CodeBlockTabsTrigger value="Linode">
                          Linode
                        </CodeBlockTabsTrigger>

                        <CodeBlockTabsTrigger value="Custom">
                          Custom
                        </CodeBlockTabsTrigger>
                      </CodeBlockTabsList>

                      <CodeBlockTab value="AWS S3">
                        ```ts
                        import { aws } from '@better-upload/server/clients';

                        const s3 = aws({
                          accessKeyId: 'your-access-key-id',
                          secretAccessKey: 'your-secret-access-key',
                          region: 'us-east-1',
                        });
                        ```
                      </CodeBlockTab>

                      <CodeBlockTab value="Cloudflare R2">
                        ```ts
                        import { cloudflare } from '@better-upload/server/clients';

                        const s3 = cloudflare({
                          accountId: 'your-account-id',
                          accessKeyId: 'your-access-key-id',
                          secretAccessKey: 'your-secret-access-key',
                        });
                        ```
                      </CodeBlockTab>

                      <CodeBlockTab value="Tigris">
                        ```ts
                        import { tigris } from '@better-upload/server/clients';

                        const s3 = tigris({
                          accessKeyId: 'your-access-key-id',
                          secretAccessKey: 'your-secret-access-key',
                          endpoint: '...', // optional
                        });
                        ```
                      </CodeBlockTab>

                      <CodeBlockTab value="Backblaze B2">
                        ```ts
                        import { backblaze } from '@better-upload/server/clients';

                        const s3 = backblaze({
                          region: 'your-backblaze-region',
                          applicationKeyId: 'your-application-key-id',
                          applicationKey: 'your-application-key',
                        });
                        ```
                      </CodeBlockTab>

                      <CodeBlockTab value="DigitalOcean Spaces">
                        ```ts
                        import { digitalOcean } from '@better-upload/server/clients';

                        const s3 = digitalOcean({
                          region: 'your-spaces-region',
                          key: 'your-spaces-key',
                          secret: 'your-spaces-secret',
                        });
                        ```
                      </CodeBlockTab>

                      <CodeBlockTab value="Wasabi">
                        ```ts
                        import { wasabi } from '@better-upload/server/clients';

                        const s3 = wasabi({
                          region: 'your-wasabi-region',
                          accessKeyId: 'your-access-key-id',
                          secretAccessKey: 'your-secret-access-key',
                        });
                        ```
                      </CodeBlockTab>

                      <CodeBlockTab value="MinIO">
                        ```ts
                        import { minio } from '@better-upload/server/clients';

                        const s3 = minio({
                          region: 'your-minio-region',
                          endpoint: 'https://minio.example.com',
                          accessKeyId: 'your-access-key-id',
                          secretAccessKey: 'your-secret-access-key',
                        });
                        ```
                      </CodeBlockTab>

                      <CodeBlockTab value="Linode">
                        ```ts
                        import { linode } from '@better-upload/server/clients';

                        const s3 = linode({
                          region: 'your-linode-region',
                          accessKey: 'your-access-key',
                          secretKey: 'your-secret-key',
                        });
                        ```
                      </CodeBlockTab>

                      <CodeBlockTab value="Custom">
                        ```ts
                        // for any S3-compatible service
                        import { custom } from '@better-upload/server/clients';

                        const s3 = custom({
                          host: 's3.us-east-1.amazonaws.com',
                          accessKeyId: 'your-access-key-id',
                          secretAccessKey: 'your-secret-access-key',
                          region: 'us-east-1',
                          secure: true,
                          forcePathStyle: false,
                        });
                        ```
                      </CodeBlockTab>
                    </CodeBlockTabs>
          </Accordion>
        </Accordions>
  </Step>

  <Step>
    ### Create `<Uploader />` component

    We will now build our UI using pre-built components. Use `<UploadDropzone />` for multiple file uploads.

    Install it via the [shadcn](https://ui.shadcn.com/) CLI:

    <CodeBlockTabs defaultValue="npm" groupId="package-manager" persist>
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="npm">
          npm
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="pnpm">
          pnpm
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="yarn">
          yarn
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="bun">
          bun
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="npm">
        ```bash
        npx shadcn@latest add @better-upload/upload-dropzone
        ```
      </CodeBlockTab>

      <CodeBlockTab value="pnpm">
        ```bash
        pnpm dlx shadcn@latest add @better-upload/upload-dropzone
        ```
      </CodeBlockTab>

      <CodeBlockTab value="yarn">
        ```bash
        yarn dlx shadcn@latest add @better-upload/upload-dropzone
        ```
      </CodeBlockTab>

      <CodeBlockTab value="bun">
        ```bash
        bun x shadcn@latest add @better-upload/upload-dropzone
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    We'll also use the `useUploadFiles` hook. The complete code looks like this:

    ```tsx title="uploader.tsx"
    'use client'; // only for Next.js

    import { useUploadFiles } from '@better-upload/client';
    import { UploadDropzone } from '@/components/ui/upload-dropzone';

    export function Uploader() {
      const { control } = useUploadFiles({
        route: 'images',
      });

      return (
        <UploadDropzone
          control={control}
          accept="image/*"
          description={{
            maxFiles: 4,
            maxFileSize: '5MB',
            fileTypes: 'JPEG, PNG, GIF',
          }}
        />
      );
    }
    ```

    Learn more about the hooks [here](/docs/hooks-multiple).
  </Step>

  <Step>
    ### Place the component

    Now place the `<Uploader />` component in your app.

    ```tsx title="page.tsx"
    import { Uploader } from '@/components/uploader';

    export default function Page() {
      return (
        <main className="flex min-h-screen flex-col items-center justify-center">
          <Uploader />
        </main>
      );
    }
    ```
  </Step>

  <Step>
    ### You're done! 🎉

    You can now run your app and upload images directly to any S3-compatible service!

    If you plan on uploading files larger than **5GB**, take a look at [multipart uploads](/docs/routes-multiple#multipart-uploads).

    <Accordions>
      <Accordion title="CORS Configuration">
        Make sure to also correctly configure CORS on your bucket. Here is an example:

        ```json
        [
          {
            "AllowedOrigins": [
              "http://localhost:3000",
              "https://example.com" // Add your domain here
            ],
            "AllowedMethods": ["GET", "PUT", "POST", "DELETE"],
            "AllowedHeaders": ["*"],
            "ExposeHeaders": ["ETag"]
          }
        ]
        ```

        Learn more about CORS [here](https://docs.aws.amazon.com/AmazonS3/latest/userguide/ManageCorsUsing.html).
      </Accordion>
    </Accordions>
  </Step>
</Steps>

## Learn more

### Concepts

<Cards>
  <Card href="/docs/routes-multiple" title="Upload routes" description="Configure upload routes for different behaviors." />

  <Card href="/docs/hooks-multiple" title="Client hooks" description="Use client-side hooks to easily upload files." />
</Cards>

### Guides

<Cards>
  <Card href="/docs/guides/forms/react-hook-form" title="React Hook Form" description="Add file uploads to forms built with React Hook Form." />

  <Card href="/docs/guides/forms/tanstack-form" title="TanStack Form" description="Add file uploads to forms built with TanStack Form." />

  <Card href="/docs/guides/tanstack-query" title="TanStack Query" description="Use TanStack Query to manage the upload process." />
</Cards>

### Components

<Cards>
  <Card href="/docs/components/upload-button" title="Upload button" description="A button that uploads a single file." />

  <Card href="/docs/components/upload-dropzone" title="Upload dropzone" description="A dropzone that uploads multiple files." />

  <Card href="/docs/components/upload-progress" title="Upload progress" description="A component to display upload progress for files being uploaded." />

  <Card href="/docs/components/paste-upload-area" title="Paste upload area" description="A wrapper component that enables file uploads via paste events." />
</Cards>
