# Helpers (/docs/helpers-client)
Format bytes [#format-bytes]
Format a number of bytes into a human-readable string.
```ts
import { formatBytes } from '@better-upload/client/helpers';
formatBytes(1000); // "1 kB"
formatBytes(1000, { decimalPlaces: 2 }); // "1.00 kB"
formatBytes(1024, { si: false }); // "1 KiB"
formatBytes(1024, { decimalPlaces: 2, si: false }); // "1.00 KiB"
```
# Helpers (/docs/helpers-server)
S3 Clients [#s3-clients]
Better Upload has built-in clients for popular S3-compatible storage services, like AWS S3 and Cloudflare R2. Suggest a new client by [opening an issue](https://github.com/Nic13Gamer/better-upload/issues).
AWS S3
Cloudflare R2
Tigris
Backblaze B2
DigitalOcean Spaces
Wasabi
MinIO
Linode
Custom
```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',
});
```
```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',
});
```
```ts
import { tigris } from '@better-upload/server/clients';
const s3 = tigris({
accessKeyId: 'your-access-key-id',
secretAccessKey: 'your-secret-access-key',
endpoint: '...', // optional
});
```
```ts
import { backblaze } from '@better-upload/server/clients';
const s3 = backblaze({
region: 'your-backblaze-region',
applicationKeyId: 'your-application-key-id',
applicationKey: 'your-application-key',
});
```
```ts
import { digitalOcean } from '@better-upload/server/clients';
const s3 = digitalOcean({
region: 'your-spaces-region',
key: 'your-spaces-key',
secret: 'your-spaces-secret',
});
```
```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',
});
```
```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',
});
```
```ts
import { linode } from '@better-upload/server/clients';
const s3 = linode({
region: 'your-linode-region',
accessKey: 'your-access-key',
secretKey: 'your-secret-key',
});
```
```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,
});
```
You can omit the parameters and let the client get your credentials from
environment variables.
Objects [#objects]
Helpers for working with objects in S3-compatible storage services. Suggest new helpers by [opening an issue](https://github.com/Nic13Gamer/better-upload/issues).
Presign get object [#presign-get-object]
Generate a pre-signed URL to download an object. Commonly used for client-side downloads.
```ts
import { presignGetObject } from '@better-upload/server/helpers';
const url = await presignGetObject(s3, {
bucket: 'my-bucket',
key: 'example.png',
expiresIn: 3600, // 1 hour
});
```
Get object [#get-object]
Get an object (including its content) from an S3 bucket.
```ts
import { getObjectBlob, getObjectStream } from '@better-upload/server/helpers';
// as a Blob
const objectBlob = await getObjectBlob(s3, {
bucket: 'my-bucket',
key: 'example.png',
});
// as a ReadableStream
const objectStream = await getObjectStream(s3, {
bucket: 'my-bucket',
key: 'example.png',
});
```
Head object [#head-object]
Get metadata about an object without fetching its content.
```ts
import { headObject } from '@better-upload/server/helpers';
const object = await headObject(s3, {
bucket: 'my-bucket',
key: 'example.png',
});
```
List objects V2 [#list-objects-v2]
List objects in an S3 bucket, using the [ListObjectsV2 command](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectsV2.html).
```ts
import { listObjectsV2 } from '@better-upload/server/helpers';
const { contents } = await listObjectsV2(s3, {
bucket: 'my-bucket',
});
```
Move object (rename) [#move-object-rename]
Move an object from one key to another, within or between, S3 buckets. Also known as renaming.
```ts
import { moveObject } from '@better-upload/server/helpers';
await moveObject(s3, {
source: {
bucket: 'source-bucket',
key: 'example.png',
},
destination: {
bucket: 'destination-bucket',
key: 'copy.png',
},
});
```
This copies the object to the new location and then deletes the original
object. It can be slow.
Copy object [#copy-object]
Copy an object, within or between, S3 buckets.
```ts
import { copyObject } from '@better-upload/server/helpers';
await copyObject(s3, {
source: {
bucket: 'source-bucket',
key: 'example.png',
},
destination: {
bucket: 'destination-bucket',
key: 'images/example.png',
},
});
```
Put object [#put-object]
Upload an object to an S3 bucket.
```ts
import { putObject } from '@better-upload/server/helpers';
await putObject(s3, {
bucket: 'my-bucket',
key: 'example.txt',
body: 'Hello, world!',
contentType: 'text/plain',
});
```
Do not use this helper for large files or for client-side uploads (use the
standard router instead).
Delete object [#delete-object]
Delete an object from an S3 bucket.
```ts
import { deleteObject } from '@better-upload/server/helpers';
await deleteObject(s3, {
bucket: 'my-bucket',
key: 'example.png',
});
```
Delete objects [#delete-objects]
Delete multiple objects from an S3 bucket in a single request.
```ts
import { deleteObjects } from '@better-upload/server/helpers';
const { deleted, errors } = await deleteObjects(s3, {
bucket: 'my-bucket',
objects: [{ key: 'file1.png' }, { key: 'file2.png' }],
});
```
Multipart uploads [#multipart-uploads]
Helpers for working with multipart uploads in S3-compatible storage services.
Create multipart upload [#create-multipart-upload]
Create a new multipart upload in an S3 bucket.
```ts
import { createMultipartUpload } from '@better-upload/server/helpers';
const { uploadId } = await createMultipartUpload(s3, {
bucket: 'my-bucket',
key: 'large-file.zip',
contentType: 'application/zip',
});
```
Upload part [#upload-part]
Upload a part of a multipart upload.
```ts
import { uploadPart } from '@better-upload/server/helpers';
const { eTag } = await uploadPart(s3, {
bucket: 'my-bucket',
key: 'large-file.zip',
uploadId: '...',
partNumber: 1,
body: partData,
});
```
Complete multipart upload [#complete-multipart-upload]
Complete a multipart upload in an S3 bucket.
```ts
import { completeMultipartUpload } from '@better-upload/server/helpers';
await completeMultipartUpload(s3, {
bucket: 'my-bucket',
key: 'large-file.zip',
uploadId: '...',
parts: [
{ partNumber: 1, eTag: '...' },
{ partNumber: 2, eTag: '...' },
// ...
],
});
```
Abort multipart upload [#abort-multipart-upload]
Abort a multipart upload in an S3 bucket.
```ts
import { abortMultipartUpload } from '@better-upload/server/helpers';
await abortMultipartUpload(s3, {
bucket: 'my-bucket',
key: 'large-file.zip',
uploadId: '...',
});
```
Object tagging operations [#object-tagging-operations]
Helpers for managing object tags in S3-compatible storage services.
Get object tagging [#get-object-tagging]
Get the tags of an S3 object.
```ts
import { getObjectTagging } from '@better-upload/server/helpers';
const { tags, tagsObject } = await getObjectTagging(s3, {
bucket: 'my-bucket',
key: 'example.png',
});
```
Put object tagging [#put-object-tagging]
Set the tags for an S3 object.
```ts
import { putObjectTagging } from '@better-upload/server/helpers';
await putObjectTagging(s3, {
bucket: 'my-bucket',
key: 'example.png',
tagging: {
tag1: 'value1',
tag2: 'value2',
},
});
```
Delete object tagging [#delete-object-tagging]
Delete all tags from an S3 object.
```ts
import { deleteObjectTagging } from '@better-upload/server/helpers';
await deleteObjectTagging(s3, {
bucket: 'my-bucket',
key: 'example.png',
});
```
# Client Hooks (/docs/hooks-multiple)
Better Upload provides React hooks that allow you to easily upload files using pre-signed URLs. Multipart uploads are managed automatically, in case you enable them in the server.
Usage [#usage]
```tsx title="uploader.tsx"
import { useUploadFiles } from '@better-upload/client';
export function Uploader() {
const {
upload,
uploadAsync,
reset,
uploadedFiles,
failedFiles,
progresses,
isPending,
isAborted,
isSettled,
isError,
allSucceeded,
hasFailedFiles,
averageProgress,
error,
metadata,
control, // for use in pre-built components
} = useUploadFiles({
route: 'images',
});
return (
{
if (e.target.files) {
upload(e.target.files);
}
}}
/>
);
}
```
If your upload route handler is not located at `/api/upload`, you need to
specify the correct path in the `api` option.
Options [#options]
The `useUploadFiles` hook accepts the following options:
Events [#events]
On before upload [#on-before-upload]
Callback that is called before requesting the pre-signed URLs. Use this to modify the files before uploading them, like resizing or compressing. You can also throw an error to reject the file upload.
```tsx
useUploadFiles({
route: 'images',
onBeforeUpload: ({ files }) => {
// rename all files
return files.map(
(file) => new File([file], 'renamed-' + file.name, { type: file.type })
);
},
});
```
On upload begin [#on-upload-begin]
Event that is called before the files start being uploaded to S3. This happens after the server responds with the pre-signed URLs.
```tsx
useUploadFiles({
route: 'images',
onUploadBegin: ({ files, metadata }) => {
console.log('Upload begin');
},
});
```
On upload progress [#on-upload-progress]
Event that is called when a file upload progress changes.
```tsx
useUploadFiles({
route: 'images',
onUploadProgress: ({ file }) => {
console.log(`${file.name} upload progress: ${file.progress * 100}%`);
},
});
```
On upload complete [#on-upload-complete]
Event that is called after files are successfully uploaded.
```tsx
// This event is called even if some files fail to upload, but some succeed.
// This event is not called if all files fail to upload.
useUploadFiles({
route: 'images',
onUploadComplete: ({ files, failedFiles, metadata }) => {
console.log(`${files.length} files uploaded`);
},
});
```
On upload fail [#on-upload-fail]
Event that is called after the entire upload if a file fails to upload.
```tsx
// This event is called even if some files succeed to upload, but some fail.
// This event is not called if all files succeed.
useUploadFiles({
route: 'images',
onUploadFail: ({ succeededFiles, failedFiles, metadata }) => {
console.log('Some or all files failed to upload');
},
});
```
On error [#on-error]
Event that is called if a critical error occurs before the upload to S3, and no files were able to be uploaded. For example, if your server is unreachable.
```tsx
useUploadFiles({
route: 'images',
onError: (error) => {
console.log(error.message);
},
});
```
This event is also called if some input is invalid. For example, if no files
were selected.
On upload settle [#on-upload-settle]
Event that is called after the upload settles (either successfully completed or an error occurs).
```tsx
useUploadFiles({
route: 'images',
onUploadSettle: ({ files, failedFiles, metadata }) => {
console.log('Upload settled');
},
});
```
Metadata [#metadata]
It is possible to send metadata from the client to your server on the upload request.
```tsx
export function Uploader() {
const { upload } = useUploadFiles({
route: 'images',
});
return (
{
if (e.target.files) {
upload(e.target.files, {
// [!code highlight:3]
metadata: {
folder: 'my-folder',
},
});
}
}}
/>
);
}
```
You can validate the client metadata on the server by setting the
`clientMetadataSchema` option in the upload route.
# Client Hooks (/docs/hooks-single)
Better Upload provides React hooks that allow you to easily upload files using pre-signed URLs. Multipart uploads are managed automatically, in case you enable them in the server.
Usage [#usage]
```tsx title="uploader.tsx"
import { useUploadFile } from '@better-upload/client';
export function Uploader() {
const {
upload,
uploadAsync,
reset,
uploadedFile,
progress,
isPending,
isAborted,
isSettled,
isError,
isSuccess,
error,
metadata,
control, // for use in pre-built components
} = useUploadFile({
route: 'profile',
});
return (
{
if (e.target.files?.[0]) {
upload(e.target.files[0]);
}
}}
/>
);
}
```
If your upload route handler is not located at `/api/upload`, you need to
specify the correct path in the `api` option.
Options [#options]
The `useUploadFile` hook accepts the following options:
Events [#events]
On before upload [#on-before-upload]
Callback that is called before requesting the pre-signed URL. Use this to modify the file before uploading it, like resizing or compressing. You can also throw an error to reject the file upload.
```tsx
useUploadFile({
route: 'profile',
onBeforeUpload: ({ file }) => {
// rename the file
return new File([file], 'renamed-' + file.name, { type: file.type });
},
});
```
On upload begin [#on-upload-begin]
Event that is called before the file starts being uploaded to S3. This happens after the server responds with the pre-signed URL.
```tsx
useUploadFile({
route: 'profile',
onUploadBegin: ({ file, metadata }) => {
console.log('Upload begin');
},
});
```
On upload progress [#on-upload-progress]
Event that is called when the file upload progress changes.
```tsx
useUploadFile({
route: 'profile',
onUploadProgress: ({ file }) => {
console.log(`Upload progress: ${file.progress * 100}%`);
},
});
```
On upload complete [#on-upload-complete]
Event that is called after the file is successfully uploaded.
```tsx
useUploadFile({
route: 'profile',
onUploadComplete: ({ file, metadata }) => {
console.log('File uploaded');
},
});
```
On error [#on-error]
Event that is called if the upload fails.
```tsx
useUploadFile({
route: 'profile',
onError: (error) => {
console.log(error.message);
},
});
```
This event is also called if some input is invalid. For example, if no files
were selected.
On upload settle [#on-upload-settle]
Event that is called after the upload settles (either successfully completed or an error occurs).
```tsx
useUploadFile({
route: 'profile',
onUploadSettle: ({ file, metadata }) => {
console.log('Upload settled');
},
});
```
Metadata [#metadata]
It is possible to send metadata from the client to your server on the upload request.
```tsx
export function Uploader() {
const { upload } = useUploadFile({
route: 'profile',
});
return (
{
if (e.target.files?.[0]) {
upload(e.target.files[0], {
// [!code highlight:3]
metadata: {
folder: 'my-folder',
},
});
}
}}
/>
);
}
```
You can validate the client metadata on the server by setting the
`clientMetadataSchema` option in the upload route.
# Introduction (/docs)
Why? [#why]
After needing to implement file uploads from scratch in multiple projects, I realized that there was a need for a better and simpler way to set up file uploads in React while still owning my S3 bucket. I wanted a library that was easy to use, fast to set up, and not bloated with features I didn't need. So I made Better Upload.
Universal [#universal]
Better Upload is framework-agnostic, meaning you can use it with any React framework, like [Next.js](https://nextjs.org), [Remix](https://remix.run), [TanStack Start](https://tanstack.com/start), or any other. It also works with any separate backend server, like [Hono](https://hono.dev), [Elysia](https://elysiajs.com), [Express](https://expressjs.com) and [Fastify](https://fastify.dev).
Developer Experience [#developer-experience]
When creating Better Upload, my main focus was on making the developer experience as smooth as possible.
* **Fast to set up:** It takes only a few minutes to get started and upload files directly to your S3 bucket.
* **Beautiful:** There are copy-and-paste [shadcn/ui](https://ui.shadcn.com) components that you can use to rapidly build your UI.
* **Own your data:** Upload directly to your S3 bucket, so you have full control over files.
LLMs [#llms]
AI agents can access the Better Upload documentation in Markdown by:
* **Full docs:** [`llms-full.txt`](https://better-upload.com/llms-full.txt) - contains the complete documentation in plain Markdown.
* **Per-page content:** Every page in the docs is available as `.mdx`. For example, [`/docs/quickstart.mdx`](https://better-upload.com/docs/quickstart.mdx)
# Quickstart (/docs/quickstart-single)
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.
Uploading your first image [#uploading-your-first-image]
Install [#install]
Install the `@better-upload/server` and `@better-upload/client` packages.
npm
pnpm
yarn
bun
```bash
npm i @better-upload/server @better-upload/client
```
```bash
pnpm add @better-upload/server @better-upload/client
```
```bash
yarn add @better-upload/server @better-upload/client
```
```bash
bun add @better-upload/server @better-upload/client
```
Set up server [#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).
Next.js
TanStack Start
Remix
Hono
Elysia
Express
Fastify
```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: {
profile: route({
fileTypes: ['image/*'],
}),
},
};
export const { POST } = toRouteHandler(router);
```
```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: {
profile: route({
fileTypes: ['image/*'],
}),
},
};
export const Route = createFileRoute('/api/upload')({
server: {
handlers: {
POST: async ({ request }) => {
return handleRequest(request, router);
},
},
},
});
```
```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: {
profile: route({
fileTypes: ['image/*'],
}),
},
};
export async function action({ request }: ActionFunctionArgs) {
return handleRequest(request, router);
}
```
```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: {
profile: route({
fileTypes: ['image/*'],
}),
},
};
const app = new Hono();
app.post('/upload', (c) => {
return handleRequest(c.req.raw, router);
});
export default app;
```
```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: {
profile: route({
fileTypes: ['image/*'],
}),
},
};
const app = new Elysia()
.post('/upload', ({ request }) => {
return handleRequest(request, router);
})
.listen(3000);
```
```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: {
profile: route({
fileTypes: ['image/*'],
}),
},
};
const app = express();
app.post('/upload', toNodeHandler(router));
// only mount express json middleware AFTER upload router
app.use(express.json());
app.listen(3000);
```
```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: {
profile: route({
fileTypes: ['image/*'],
}),
},
};
const app = Fastify();
app.post('/upload', toNodeHandler(router));
app.listen({ port: 3000 });
```
In the example above, we create the upload route `profile`. Learn more about upload routes [here](/docs/routes-single).
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: {
profile: route({
fileTypes: ['image/*'],
// [!code ++:7]
onBeforeUpload: async ({ req, file, clientMetadata }) => {
const user = await auth(req);
if (!user) {
throw new RejectUpload('Not logged in!');
}
},
}),
},
};
```
You can modify the S3 object through the `onBeforeUpload` callback:
```ts
const router: Router = {
client: aws(),
bucketName: 'my-bucket',
routes: {
profile: route({
fileTypes: ['image/*'],
// [!code ++:10]
onBeforeUpload: async ({ req, file, clientMetadata }) => {
return {
objectInfo: {
key: `files/${file.name}`,
metadata: {
author: 'user_123',
},
},
};
},
}),
},
};
```
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).
AWS S3
Cloudflare R2
Tigris
Backblaze B2
DigitalOcean Spaces
Wasabi
MinIO
Linode
Custom
```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',
});
```
```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',
});
```
```ts
import { tigris } from '@better-upload/server/clients';
const s3 = tigris({
accessKeyId: 'your-access-key-id',
secretAccessKey: 'your-secret-access-key',
endpoint: '...', // optional
});
```
```ts
import { backblaze } from '@better-upload/server/clients';
const s3 = backblaze({
region: 'your-backblaze-region',
applicationKeyId: 'your-application-key-id',
applicationKey: 'your-application-key',
});
```
```ts
import { digitalOcean } from '@better-upload/server/clients';
const s3 = digitalOcean({
region: 'your-spaces-region',
key: 'your-spaces-key',
secret: 'your-spaces-secret',
});
```
```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',
});
```
```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',
});
```
```ts
import { linode } from '@better-upload/server/clients';
const s3 = linode({
region: 'your-linode-region',
accessKey: 'your-access-key',
secretKey: 'your-secret-key',
});
```
```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,
});
```
Create `` component [#create-uploader--component]
We will now build our UI using pre-built components. We'll use `` for single file uploads.
Install it via the [shadcn](https://ui.shadcn.com/) CLI:
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add @better-upload/upload-button
```
```bash
pnpm dlx shadcn@latest add @better-upload/upload-button
```
```bash
yarn dlx shadcn@latest add @better-upload/upload-button
```
```bash
bun x shadcn@latest add @better-upload/upload-button
```
We'll also use the `useUploadFile` hook. The complete code looks like this:
```tsx title="uploader.tsx"
'use client'; // only for Next.js
import { useUploadFile } from '@better-upload/client';
import { UploadButton } from '@/components/ui/upload-button';
export function Uploader() {
const { control } = useUploadFile({
route: 'profile',
});
return ;
}
```
Learn more about the hooks [here](/docs/hooks-single).
Place the component [#place-the-component]
Now place the `` component in your app.
```tsx title="page.tsx"
import { Uploader } from '@/components/uploader';
export default function Page() {
return (
);
}
```
You're done! 🎉 [#youre-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-single#multipart-uploads).
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).
Learn more [#learn-more]
Concepts [#concepts]
Guides [#guides]
Components [#components]
# 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.
Uploading images [#uploading-images]
Install [#install]
Install the `@better-upload/server` and `@better-upload/client` packages.
npm
pnpm
yarn
bun
```bash
npm i @better-upload/server @better-upload/client
```
```bash
pnpm add @better-upload/server @better-upload/client
```
```bash
yarn add @better-upload/server @better-upload/client
```
```bash
bun add @better-upload/server @better-upload/client
```
Set up server [#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).
Next.js
TanStack Start
Remix
Hono
Elysia
Express
Fastify
```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);
```
```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);
},
},
},
});
```
```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);
}
```
```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;
```
```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);
```
```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);
```
```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 });
```
In the example above, we create the upload route `images`. Learn more about upload routes [here](/docs/routes-multiple).
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!');
}
},
}),
},
};
```
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',
},
}),
};
},
}),
},
};
```
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).
AWS S3
Cloudflare R2
Tigris
Backblaze B2
DigitalOcean Spaces
Wasabi
MinIO
Linode
Custom
```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',
});
```
```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',
});
```
```ts
import { tigris } from '@better-upload/server/clients';
const s3 = tigris({
accessKeyId: 'your-access-key-id',
secretAccessKey: 'your-secret-access-key',
endpoint: '...', // optional
});
```
```ts
import { backblaze } from '@better-upload/server/clients';
const s3 = backblaze({
region: 'your-backblaze-region',
applicationKeyId: 'your-application-key-id',
applicationKey: 'your-application-key',
});
```
```ts
import { digitalOcean } from '@better-upload/server/clients';
const s3 = digitalOcean({
region: 'your-spaces-region',
key: 'your-spaces-key',
secret: 'your-spaces-secret',
});
```
```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',
});
```
```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',
});
```
```ts
import { linode } from '@better-upload/server/clients';
const s3 = linode({
region: 'your-linode-region',
accessKey: 'your-access-key',
secretKey: 'your-secret-key',
});
```
```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,
});
```
Create `` component [#create-uploader--component]
We will now build our UI using pre-built components. Use `` for multiple file uploads.
Install it via the [shadcn](https://ui.shadcn.com/) CLI:
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add @better-upload/upload-dropzone
```
```bash
pnpm dlx shadcn@latest add @better-upload/upload-dropzone
```
```bash
yarn dlx shadcn@latest add @better-upload/upload-dropzone
```
```bash
bun x shadcn@latest add @better-upload/upload-dropzone
```
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 (
);
}
```
Learn more about the hooks [here](/docs/hooks-multiple).
Place the component [#place-the-component]
Now place the `` component in your app.
```tsx title="page.tsx"
import { Uploader } from '@/components/uploader';
export default function Page() {
return (
);
}
```
You're done! 🎉 [#youre-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).
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).
Learn more [#learn-more]
Concepts [#concepts]
Guides [#guides]
Components [#components]
# Upload Routes (/docs/routes-multiple)
Upload routes are where you define how files are uploaded. To define a route, use the `route` function.
You can create multiple routes for different purposes (e.g. images, videos).
Here is a basic example of a multiple file upload route:
```ts
import { route } from '@better-upload/server';
route({
multipleFiles: true,
fileTypes: ['image/*'], // Accepts all image types
maxFileSize: 1024 * 1024 * 4, // 4MB
});
```
Multiple file routes have the following options:
Callbacks [#callbacks]
When defining a route, you may want to run code before or after the upload. You can do this by using the callbacks.
Before upload [#before-upload]
The `onBeforeUpload` callback is called before pre-signed URLs are generated. Use this to run custom logic before uploading files, such as auth and rate-limiting.
The request, files, and metadata sent from the client are available.
```ts
route({
onBeforeUpload: async ({ req, files, clientMetadata }) => {
const user = await auth();
if (!user) {
throw new RejectUpload('Not logged in!');
}
return {
generateObjectInfo: ({ file }) => ({
key: `${user.id}/${file.name}`,
}),
bucketName: 'another-bucket',
};
},
});
```
Throw `RejectUpload` to reject the file upload. This will also send the error
message to the client.
You can return an object with the following properties:
After generating pre-signed URL [#after-generating-pre-signed-url]
The `onAfterSignedUrl` callback is called after the pre-signed URLs are generated. Use this to run custom logic after the URL is generated, such as logging and saving data.
In addition to all previous data, metadata from the `onBeforeUpload` callback is also available.
```ts
route({
onAfterSignedUrl: async ({ req, files, metadata, clientMetadata }) => {
// the files now have the objectInfo property
return {
metadata: {
example: '123',
},
};
},
});
```
You can return an object with the following properties:
Multipart uploads [#multipart-uploads]
If you want to upload files larger than **5GB**, you must use multipart uploads. To enable it, set `multipart` to `true`. It works both for single and multiple files. No change is needed in the client.
```ts
route({
multipleFiles: true,
multipart: true, // [!code highlight]
partSize: 1024 * 1024 * 20, // 20MB, default is 50MB [!code highlight]
});
```
You can now also modify the following options:
Even for files under 5GB, using multipart uploads can speed up the upload
process. Empty files (0 bytes) will fail to upload with multipart uploads.
Router [#router]
The router is where all upload routes are defined. To define a router, create an object with the `Router` type.
```ts
import { type Router } from '@better-upload/server';
export const router: Router = {
client: s3,
bucketName: 'my-bucket',
routes: {
// your routes...
},
};
```
# Upload Routes (/docs/routes-single)
Upload routes are where you define how files are uploaded. To define a route, use the `route` function.
You can create multiple routes for different purposes (e.g. images, videos).
Here is a basic example of a single file upload route:
```ts
import { route } from '@better-upload/server';
route({
fileTypes: ['image/*'], // Accepts all image types
maxFileSize: 1024 * 1024 * 4, // 4MB
});
```
Single file routes have the following options:
Callbacks [#callbacks]
When defining a route, you may want to run code before or after the upload. You can do this by using the callbacks.
Before upload [#before-upload]
The `onBeforeUpload` callback is called before the pre-signed URL is generated. Use this to run custom logic before uploading a file, such as auth and rate-limiting.
The request, file, and metadata sent from the client are available.
```ts
route({
onBeforeUpload: async ({ req, file, clientMetadata }) => {
const user = await auth();
if (!user) {
throw new RejectUpload('Not logged in!');
}
return {
objectInfo: {
key: user.id,
},
bucketName: 'another-bucket',
};
},
});
```
Throw `RejectUpload` to reject the file upload. This will also send the error
message to the client.
You can return an object with the following properties:
After generating pre-signed URL [#after-generating-pre-signed-url]
The `onAfterSignedUrl` callback is called after the pre-signed URL is generated. Use this to run custom logic after the URL is generated, such as logging and saving data.
In addition to all previous data, metadata from the `onBeforeUpload` callback is also available.
```ts
route({
onAfterSignedUrl: async ({ req, file, metadata, clientMetadata }) => {
// the file now has the objectInfo property
return {
metadata: {
example: '123',
},
};
},
});
```
You can return an object with the following properties:
Multipart uploads [#multipart-uploads]
If you want to upload files larger than **5GB**, you must use multipart uploads. To enable it, set `multipart` to `true`. It works both for single and multiple files. No change is needed in the client.
```ts
route({
multipleFiles: true,
multipart: true, // [!code highlight]
partSize: 1024 * 1024 * 20, // 20MB, default is 50MB [!code highlight]
});
```
You can now also modify the following options:
Even for files under 5GB, using multipart uploads can speed up the upload
process. Empty files (0 bytes) will fail to upload with multipart uploads.
Router [#router]
The router is where all upload routes are defined. To define a router, create an object with the `Router` type.
```ts
import { type Router } from '@better-upload/server';
export const router: Router = {
client: s3,
bucketName: 'my-bucket',
routes: {
// your routes...
},
};
```
# Paste Upload Area (/docs/components/paste-upload-area)
Preview [#preview]
Installation [#installation]
```bash
npx shadcn@latest add @better-upload/paste-upload-area
```
**Copy and paste the following code into your project.**
```tsx title='components/ui/paste-upload-area.tsx'
import type { UploadHookControl } from '@better-upload/client';
type PasteUploadAreaProps = {
children: React.ReactNode;
control: UploadHookControl;
metadata?: Record;
uploadOverride?: (
...args: Parameters['upload']>
) => void;
// Add any additional props you need.
};
export function PasteUploadArea({
children,
control: { upload, isPending },
metadata,
uploadOverride,
}: PasteUploadAreaProps) {
return (
);
}
```
**Update the import paths to match your project setup.**
Usage [#usage]
The `` component should be used with the `useUploadFiles` hook.
```tsx
'use client';
import { useUploadFiles } from '@better-upload/client';
import { PasteUploadArea } from '@/components/ui/paste-upload-area';
export function Uploader() {
const { control, isPending } = useUploadFiles({
route: 'images',
});
return (
);
}
```
Files pasted within any child element of the component will be uploaded to the desired route.
Props [#props]
# Upload Button (/docs/components/upload-button)
Preview [#preview]
Installation [#installation]
```bash
npx shadcn@latest add @better-upload/upload-button
```
**Install the following dependencies:**
npm
pnpm
yarn
bun
```bash
npm i lucide-react
```
```bash
pnpm add lucide-react
```
```bash
yarn add lucide-react
```
```bash
bun add lucide-react
```
Also add the [shadcn/ui button](https://ui.shadcn.com/docs/components/button) component to your project. As the upload button is built on top of it.
**Copy and paste the following code into your project.**
```tsx title='components/ui/upload-button.tsx'
import { Button } from '@/components/ui/button';
import type { UploadHookControl } from '@better-upload/client';
import { Loader2, Upload } from 'lucide-react';
import { useId } from 'react';
type UploadButtonProps = {
control: UploadHookControl;
id?: string;
accept?: string;
metadata?: Record;
uploadOverride?: (
...args: Parameters['upload']>
) => void;
// Add any additional props you need.
};
export function UploadButton({
control: { upload, isPending },
id: _id,
accept,
metadata,
uploadOverride,
}: UploadButtonProps) {
const id = useId();
return (
);
}
```
**Update the import paths to match your project setup.**
Usage [#usage]
The `` component should be used with the `useUploadFile` hook.
```tsx
'use client';
import { useUploadFile } from '@better-upload/client';
import { UploadButton } from '@/components/ui/upload-button';
export function Uploader() {
const { control } = useUploadFile({
route: 'profile',
});
return ;
}
```
The button will open a file picker dialog when clicked, and upload the selected file to the desired route.
Props [#props]
# Upload Dropzone (/docs/components/upload-dropzone)
Preview [#preview]
Installation [#installation]
```bash
npx shadcn@latest add @better-upload/upload-dropzone
```
**Install the following dependencies:**
npm
pnpm
yarn
bun
```bash
npm i lucide-react react-dropzone
```
```bash
pnpm add lucide-react react-dropzone
```
```bash
yarn add lucide-react react-dropzone
```
```bash
bun add lucide-react react-dropzone
```
Make sure to have [shadcn/ui](https://ui.shadcn.com/docs/installation) set up in your project.
**Copy and paste the following code into your project.**
```tsx title='components/ui/upload-dropzone.tsx'
import { cn } from '@/lib/utils';
import type { UploadHookControl } from '@better-upload/client';
import { Loader2, Upload } from 'lucide-react';
import { useId } from 'react';
import { useDropzone } from 'react-dropzone';
type UploadDropzoneProps = {
control: UploadHookControl;
id?: string;
accept?: string;
metadata?: Record;
description?:
| {
fileTypes?: string;
maxFileSize?: string;
maxFiles?: number;
}
| string;
uploadOverride?: (
...args: Parameters['upload']>
) => void;
// Add any additional props you need.
};
export function UploadDropzone({
control: { upload, isPending },
id: _id,
accept,
metadata,
description,
uploadOverride,
}: UploadDropzoneProps) {
const id = useId();
const { getRootProps, getInputProps, isDragActive, inputRef } = useDropzone({
onDrop: (files) => {
if (files.length > 0 && !isPending) {
if (uploadOverride) {
uploadOverride(files, { metadata });
} else {
upload(files, { metadata });
}
}
inputRef.current.value = '';
},
noClick: true,
});
return (
{isDragActive && (
Drop files here
)}
);
}
```
**Update the import paths to match your project setup.**
Usage [#usage]
The `` component should be used with the `useUploadFiles` hook.
```tsx
'use client';
import { useUploadFiles } from '@better-upload/client';
import { UploadDropzone } from '@/components/ui/upload-dropzone';
export function Uploader() {
const { control } = useUploadFiles({
route: 'images',
});
return ;
}
```
When clicked, the dropzone will open a file picker dialog. When selected or dropped, the files will be uploaded to the desired route.
Description [#description]
You can customize the description shown in the dropzone. You can pass a string, or an object with the following properties:
* `maxFiles`: The maximum number of files that can be uploaded.
* `maxFileSize`: The maximum size of the files that can be uploaded, use a formatted string (e.g. `10MB`).
* `fileTypes`: The file types that can be uploaded.
```tsx
```
Note that this is only cosmetic and does not enforce any restrictions
client-side.
Props [#props]
# Upload Progress (/docs/components/upload-progress)
Preview [#preview]
Installation [#installation]
```bash
npx shadcn@latest add @better-upload/upload-progress
```
**Install the following dependencies:**
npm
pnpm
yarn
bun
```bash
npm i lucide-react react-dropzone
```
```bash
pnpm add lucide-react react-dropzone
```
```bash
yarn add lucide-react react-dropzone
```
```bash
bun add lucide-react react-dropzone
```
Make sure to have [shadcn/ui](https://ui.shadcn.com/docs/installation) set up in your project, with the [progress](https://ui.shadcn.com/docs/components/progress) component installed.
**Copy and paste the following code into your project.**
```tsx title='components/ui/upload-progress.tsx'
import { Progress } from '@/components/ui/progress';
import { cn } from '@/lib/utils';
import type { UploadHookControl } from '@better-upload/client';
import { formatBytes } from '@better-upload/client/helpers';
import { Dot, File } from 'lucide-react';
type UploadProgressProps = {
control: UploadHookControl;
// Add any additional props you need.
};
export function UploadProgress({
control: { progresses },
}: UploadProgressProps) {
return (
);
}
```
**Update the import paths to match your project setup.**
Usage [#usage]
The `` component should be used with the `useUploadFiles` hook.
```tsx
'use client';
import { useUploadFiles } from '@better-upload/client';
import { UploadProgress } from '@/components/ui/upload-progress';
export function Uploader() {
const { control } = useUploadFiles({
route: 'images',
});
return ;
}
```
This component should probably be used alongside another component, such as [``](/docs/components/upload-dropzone), to trigger the file uploads.
```tsx
'use client';
import { useUploadFiles } from '@better-upload/client';
import { UploadDropzone } from '@/components/ui/upload-dropzone';
import { UploadProgress } from '@/components/ui/upload-progress';
export function Uploader() {
const { control } = useUploadFiles({
route: 'images',
});
return (
);
}
```
Props [#props]
# TanStack Query (/docs/guides/tanstack-query)
If you prefer to use TanStack Query instead of the hooks provided by Better Upload, you can do so by using the `uploadFile` and `uploadFiles` functions with the `useMutation` hook from TanStack Query.
Example [#example]
The complete code for a simple uploader is below.
Multiple files
Single files
```tsx
'use client';
import { uploadFiles } from '@better-upload/client';
import { useMutation } from '@tanstack/react-query';
export function Uploader() {
const { mutate: upload, isPending } = useMutation({
mutationFn: async (files: File[]) => {
return uploadFiles({
files,
route: 'form',
onFileStateChange: ({ file }) => {
// you handle the progress of each file
console.log(file);
},
});
},
onSuccess: ({ files, failedFiles, metadata }) => {
console.log({
files,
failedFiles,
metadata,
});
},
onError: (error) => {
console.error(error);
},
});
return (
{
if (e.target.files) {
upload(Array.from(e.target.files));
}
}}
/>
);
}
```
```tsx
'use client';
import { uploadFile } from '@better-upload/client';
import { useMutation } from '@tanstack/react-query';
export function Uploader() {
const { mutate: upload, isPending } = useMutation({
mutationFn: async (file: File) => {
return uploadFile({
file,
route: 'form',
onFileStateChange: ({ file }) => {
// you handle the progress of the file
console.log(file);
},
});
},
onSuccess: ({ file, metadata }) => {
console.log({
file,
metadata,
});
},
onError: (error) => {
console.error(error);
},
});
return (
{
if (e.target.files?.[0]) {
upload(e.target.files[0]);
}
}}
/>
);
}
```
Tracking upload state [#tracking-upload-state]
Note that by directly using the upload functions, you need to track the state of each file upload yourself. This is simple to do, an example for multiple files is below.
```tsx
'use client';
import {
type FileUploadInfo,
type UploadStatus,
uploadFiles,
} from '@better-upload/client';
import { useMutation } from '@tanstack/react-query';
import { useState } from 'react';
export function Uploader() {
// [!code highlight:3]
const [uploadState, setUploadState] = useState(
() => new Map>()
);
const { mutate: upload, isPending } = useMutation({
mutationFn: async (files: File[]) => {
return uploadFiles({
files,
route: 'form',
onFileStateChange: ({ file }) => {
// [!code highlight:3]
setUploadState((prev) =>
new Map(prev).set(file.objectInfo.key, file)
);
},
});
},
});
return (
{
if (e.target.files) {
upload(Array.from(e.target.files));
}
}}
/>
);
}
```
# React Hook Form (/docs/guides/forms/react-hook-form)
It's common for file uploads to be part of a form. In this guide, we'll take a look at how to add file uploads to a form built with **React Hook Form**. This guide is based on the shadcn/ui [form guide](https://ui.shadcn.com/docs/forms/react-hook-form) and will use the [``](https://ui.shadcn.com/docs/components/field) component.
We'll cover multiple file uploads, but the same principles apply for single file uploads.
Form [#form]
Installation [#installation]
Install the following shadcn/ui components. Make sure to also have React Hook Form and Zod installed.
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add field input button
```
```bash
pnpm dlx shadcn@latest add field input button
```
```bash
yarn dlx shadcn@latest add field input button
```
```bash
bun x shadcn@latest add field input button
```
Setup upload route [#setup-upload-route]
Setup your upload route. Use your preferred framework, but for this example, we'll use 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(),
bucketName: 'my-bucket',
routes: {
form: route({
multipleFiles: true,
maxFiles: 5,
maxFileSize: 1024 * 1024 * 5, // 5MB
onBeforeUpload() {
return {
generateObjectInfo: ({ file }) => ({ key: `form/${file.name}` }),
};
},
}),
},
};
export const { POST } = toRouteHandler(router);
```
Define form schema [#define-form-schema]
We'll start by defining the shape of our form using a Zod schema. It contains two fields:
* `folderName`: For an arbitrary text input.
* `objectKeys`: For the uploaded files, stores the S3 object keys.
```tsx title="form.tsx"
import * as z from 'zod';
const formSchema = z.object({
folderName: z.string().min(1, 'Folder name is required.'),
objectKeys: z.array(z.string()).min(1, 'Upload at least one file.'),
});
```
Setup the form [#setup-the-form]
Next, we'll use the `useForm` hook from React Hook Form to create our form instance, along with the `useUploadFiles` hook to handle file uploads.
```tsx title="form.tsx"
import { useUploadFiles } from '@better-upload/client';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import * as z from 'zod';
const formSchema = z.object({
folderName: z.string().min(1, 'Folder name is required.'),
objectKeys: z.array(z.string()).min(1, 'Upload at least one file.'),
});
export function FormUploader() {
const form = useForm>({
resolver: zodResolver(formSchema),
defaultValues: {
folderName: '',
objectKeys: [],
},
});
const uploader = useUploadFiles({
route: 'form',
onUploadComplete: ({ files }) => {
form.setValue(
'objectKeys',
files.map((file) => file.objectInfo.key)
);
},
onError: (error) => {
form.setError('objectKeys', {
message: error.message || 'An error occurred.',
});
},
});
function onSubmit(data: z.infer) {
// call your API here
console.log(data);
}
return (
);
}
```
Build the form [#build-the-form]
We can now build the form using the `` component from React Hook Form and the `` component for the file uploads.
```tsx title="form.tsx"
'use client';
import { useUploadFiles } from '@better-upload/client';
import { zodResolver } from '@hookform/resolvers/zod';
import { Controller, useForm } from 'react-hook-form';
import * as z from 'zod';
import { Button } from '@/components/ui/button';
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import {
Field,
FieldError,
FieldGroup,
FieldLabel,
} from '@/components/ui/field';
import { Input } from '@/components/ui/input';
import { UploadDropzone } from '@/components/ui/upload-dropzone';
const formSchema = z.object({
folderName: z.string().min(1, 'Folder name is required.'),
objectKeys: z.array(z.string()).min(1, 'Upload at least one file.'),
});
export function FormUploader() {
const form = useForm>({
resolver: zodResolver(formSchema),
defaultValues: {
folderName: '',
objectKeys: [],
},
});
const uploader = useUploadFiles({
route: 'form',
onUploadComplete: ({ files }) => {
form.setValue(
'objectKeys',
files.map((file) => file.objectInfo.key)
);
},
onError: (error) => {
form.setError('objectKeys', {
message: error.message || 'An error occurred.',
});
},
});
function onSubmit(data: z.infer) {
// call your API here
console.log(data);
}
return (
Form UploaderUpload files to a specific folder.
);
}
```
Let's hide the dropzone after the user has uploaded files. We can do this by using the `uploadedFiles` array returned by the `useUploadFiles` hook.
```tsx
export function FormUploader() {
// ...
return (
{/* ... */}
{/* ... */}
);
}
```
Upload on form submit [#upload-on-form-submit]
In this example, we only upload the files after the user clicks on the submit button. We'll use the `uploadOverride` prop to override the default behavior of the ``.
```tsx title="form.tsx"
'use client';
import { useUploadFiles } from '@better-upload/client';
import { zodResolver } from '@hookform/resolvers/zod';
import { Controller, useForm } from 'react-hook-form';
import * as z from 'zod';
import { Button } from '@/components/ui/button';
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import {
Field,
FieldError,
FieldGroup,
FieldLabel,
} from '@/components/ui/field';
import { Input } from '@/components/ui/input';
import { UploadDropzone } from '@/components/ui/upload-dropzone';
const formSchema = z.object({
folderName: z.string().min(1, 'Folder name is required.'),
files: z.array(z.file()).min(1, 'Upload at least one file.'),
// for Zod v3: z.array(z.instanceof(File)).min(1, 'Upload at least one file.'),
});
export function FormUploader() {
const form = useForm>({
resolver: zodResolver(formSchema),
defaultValues: {
folderName: '',
files: [],
},
});
const uploader = useUploadFiles({
route: 'form',
onError: (error) => {
form.setError('files', {
message: error.message || 'An error occurred.',
});
},
});
async function onSubmit(data: z.infer) {
const { files } = await uploader.upload(data.files);
// call your API here
console.log({
folderName: data.folderName,
objectKeys: files.map((file) => file.objectInfo.key),
});
}
return (
Form UploaderUpload files to a specific folder.
);
}
```
# TanStack Form (/docs/guides/forms/tanstack-form)
It's common for file uploads to be part of a form. In this guide, we'll take a look at how to add file uploads to a form built with **TanStack Form**. This guide is based on the shadcn/ui [form guide](https://ui.shadcn.com/docs/forms/tanstack-form) and will use the [``](https://ui.shadcn.com/docs/components/field) component.
We'll cover multiple file uploads, but the same principles apply for single file uploads.
Form [#form]
Installation [#installation]
Install the following shadcn/ui components. Make sure to also have TanStack Form and Zod installed.
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add field input button
```
```bash
pnpm dlx shadcn@latest add field input button
```
```bash
yarn dlx shadcn@latest add field input button
```
```bash
bun x shadcn@latest add field input button
```
Setup upload route [#setup-upload-route]
Setup your upload route. Use your preferred framework, but for this example, we'll use 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(),
bucketName: 'my-bucket',
routes: {
form: route({
multipleFiles: true,
maxFiles: 5,
maxFileSize: 1024 * 1024 * 5, // 5MB
onBeforeUpload() {
return {
generateObjectInfo: ({ file }) => ({ key: `form/${file.name}` }),
};
},
}),
},
};
export const { POST } = toRouteHandler(router);
```
Define form schema [#define-form-schema]
We'll start by defining the shape of our form using a Zod schema. It contains two fields:
* `folderName`: For an arbitrary text input.
* `objectKeys`: For the uploaded files, stores the S3 object keys.
```tsx title="form.tsx"
import * as z from 'zod';
const formSchema = z.object({
folderName: z.string().min(1, 'Folder name is required.'),
objectKeys: z.array(z.string()).min(1, 'Upload at least one file.'),
});
```
Setup the form [#setup-the-form]
Use the `useForm` hook from TanStack Form to create your form instance with Zod validation, along with the `useUploadFiles` hook to handle file uploads.
```tsx title="form.tsx"
import { useUploadFiles } from '@better-upload/client';
import { useForm } from '@tanstack/react-form';
import * as z from 'zod';
const formSchema = z.object({
folderName: z.string().min(1, 'Folder name is required.'),
objectKeys: z.array(z.string()).min(1, 'Upload at least one file.'),
});
export function FormUploader() {
const form = useForm({
defaultValues: {
folderName: '',
objectKeys: [] as string[],
},
validators: {
onSubmit: formSchema,
},
onSubmit: ({ value }) => {
// call your API here
console.log(value);
},
});
const uploader = useUploadFiles({
route: 'form',
onUploadComplete: ({ files }) => {
form.setFieldValue(
'objectKeys',
files.map((file) => file.objectInfo.key)
);
},
});
return (
);
}
```
Build the form [#build-the-form]
We can now build the form using the `form.Field` component from TanStack Form and the `` component for the file uploads.
```tsx title="form.tsx"
'use client';
import { useUploadFiles } from '@better-upload/client';
import { useForm } from '@tanstack/react-form';
import * as z from 'zod';
import { Button } from '@/components/ui/button';
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import {
Field,
FieldError,
FieldGroup,
FieldLabel,
} from '@/components/ui/field';
import { Input } from '@/components/ui/input';
import { UploadDropzone } from '@/components/ui/upload-dropzone';
const formSchema = z.object({
folderName: z.string().min(1, 'Folder name is required.'),
objectKeys: z.array(z.string()).min(1, 'Upload at least one file.'),
});
export function FormUploader() {
const form = useForm({
defaultValues: {
folderName: '',
objectKeys: [] as string[],
},
validators: {
onSubmit: formSchema,
},
onSubmit: ({ value }) => {
// call your API here
console.log(value);
},
});
const uploader = useUploadFiles({
route: 'form',
onUploadComplete: ({ files }) => {
form.setFieldValue(
'objectKeys',
files.map((file) => file.objectInfo.key)
);
},
});
return (
Form UploaderUpload files to a specific folder.
);
}
```
Let's hide the dropzone after the user has uploaded files. We can do this by using the `uploadedFiles` array returned by the `useUploadFiles` hook.
```tsx
export function FormUploader() {
// ...
return (
{/* ... */}
{/* ... */}
);
}
```
Upload on form submit [#upload-on-form-submit]
In this example, we only upload the files after the user clicks on the submit button. We'll use the `uploadOverride` prop to override the default behavior of the ``.
```tsx title="form.tsx"
'use client';
import { useUploadFiles } from '@better-upload/client';
import { useForm } from '@tanstack/react-form';
import * as z from 'zod';
import { Button } from '@/components/ui/button';
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import {
Field,
FieldError,
FieldGroup,
FieldLabel,
} from '@/components/ui/field';
import { Input } from '@/components/ui/input';
import { UploadDropzone } from '@/components/ui/upload-dropzone';
const formSchema = z.object({
folderName: z.string().min(1, 'Folder name is required.'),
files: z.array(z.file()).min(1, 'Upload at least one file.'),
// for Zod v3: z.array(z.instanceof(File)).min(1, 'Upload at least one file.'),
});
export function FormUploader() {
const form = useForm({
defaultValues: {
folderName: '',
files: [] as File[],
},
validators: {
onSubmit: formSchema,
},
onSubmit: async ({ value }) => {
const { files } = await uploader.upload(value.files);
// call your API here
console.log({
folderName: value.folderName,
objectKeys: files.map((file) => file.objectInfo.key),
});
},
});
const uploader = useUploadFiles({
route: 'form',
});
return (
Form UploaderUpload files to a specific folder.
);
}
```