Documentation menu

 

upup vs Better Upload

The short answer: Better Upload is a small, well-focused library for React apps that upload to their own S3-compatible bucket: a typed server route signs the URLs, hooks do the upload, and you copy shadcn/ui components into your project. Keep it if that is all you need. upup is a ready-made uploader with the interface built in, native components for React, Vue, Svelte, Angular, Preact and vanilla JavaScript, cloud-drive sources, camera and screen capture, and resumable uploads that can continue after a page reload once the file is selected again.

Try it first: the live React demo uploads a real file from your browser, and the React quickstart gets it running in your app.

"Better Upload" is also written "better-upload" and "betterupload". This page compares it with upup on the interface, frameworks, large files and server model, then shows a migration. For other React options, see the React file upload libraries roundup.

Which package is Better Upload?

The current packages are @better-upload/client and @better-upload/server, at version 3.0.19 (May 2026). The unscoped better-upload package on npm, whose last version was 2.0.3 in October 2025, is deprecated in favor of them. Both scoped packages are MIT-licensed. The client declares React as a peer dependency; the server uses aws4fetch rather than the AWS SDK.

Comparison table

FeatureBetter Uploadupup
What it isReact hooks plus a server route that signs presigned URLsReady-made uploader component, with an optional server package
FrameworksReact client; server adapters for Next.js, Remix, TanStack Start, Hono, Elysia, Express, FastifyReact, Next.js, Vue, Svelte, Angular, Preact, vanilla JS
Interfaceshadcn/ui components you copy into your project (button, dropzone, paste area, progress)Built-in panel with drop zone, file list, previews and source tabs; headless hooks for a custom UI
Where files goYour S3-compatible bucket, via presigned URLsYour S3-compatible bucket, via presigned URLs
ServerRequired: a router with named routesClient mode: your own presign endpoint. Server mode: @useupup/server
Server-side file rulesPer route: fileTypes, maxFileSize, maxFiles, client metadata schema@useupup/server: maxFileSize, allowedTypes, hooks.onBeforeUpload; in client mode, your own endpoint
Large filesmultipart: true on the route; parts upload in parallelresumable: S3 multipart needs server mode; tus needs a tus endpoint
Resume after a reloadNot in its documentationYes, with multipart or tus, once the file is re-selected or restored by crashRecovery
Retriesretry and retryDelay options on the hooksmaxRetries (default 3) plus per-part retries
Cloud-drive sourcesNoGoogle Drive, OneDrive, Dropbox, Box
Camera, screen captureNoYes
Image processingYour code in the client onBeforeUpload hookOptional compression, HEIC to JPEG, EXIF stripping; editor in React and Preact
LicenseMITMIT

What Better Upload gives you

Better Upload splits the job in two. On the server you define a router of named routes with route(), for example an images route with fileTypes, maxFileSize and maxFiles. The route handler checks the request, signs presigned URLs and returns them. In React, useUploadFiles (or useUploadFile for a single file) takes the files, uploads them to the bucket and exposes progress, results and errors. The server side has callbacks for auth (onBeforeUpload, rejecting with RejectUpload), object keys and metadata, and a clientMetadataSchema that validates metadata from the client. Setting multipart: true on a route enables S3 multipart uploads for files over 5 GB, or to speed up smaller ones, with no change on the client.

The interface is a set of shadcn/ui components, Upload Button, Upload Dropzone, Paste Upload Area and Upload Progress, that you install with the shadcn CLI and then own and edit. There are built-in S3 clients for AWS S3, Cloudflare R2, Tigris, Backblaze B2, DigitalOcean Spaces, Wasabi, MinIO and Linode, plus a custom one, and server helpers for objects such as presigning downloads and listing, copying and deleting.

It is the more widely used of the two, with around 58,000 weekly npm downloads for @better-upload/client in October 2026. Its documentation does not describe cloud-drive sources, camera or screen capture, an image editor, or resuming an interrupted upload after a reload.

What upup gives you

upup ships the interface: a drop zone, file list with previews, progress and source tabs, built from one headless core and ported natively to React, Vue, Svelte, Angular, Preact and vanilla JavaScript. Users can pick files from the device, Google Drive, OneDrive, Dropbox, Box, a camera, a screen capture or a link. In client mode your own endpoint returns presigned URLs; in server mode @useupup/server signs them, enforces size and type limits, and handles cloud-drive OAuth and transfers. Large files can use S3 multipart through @useupup/server, or tus, and both can resume after a reload when the user selects the same file again, or crashRecovery restores it and they click Resume. A file that compression or HEIC conversion rewrote does not match the old session and starts over. In React and Preact there is also an image editor.

The trade-offs run the other way too. upup is the younger and smaller project. Its S3 multipart needs server mode and its upload-token secret, where Better Upload turns multipart on with one route option. And you theme upup's panel rather than editing its components, while Better Upload hands you the component source.

Migrate from Better Upload to upup

Take a Next.js App Router setup with Better Upload. The route:

ts
// app/api/upload/route.ts
import { RejectUpload, 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: {
        images: route({
            fileTypes: ['image/*'],
            multipleFiles: true,
            maxFiles: 4,
            maxFileSize: 1024 * 1024 * 4,
            onBeforeUpload: async ({ req }) => {
                const user = await getSessionUser(req)
                if (!user) throw new RejectUpload('Not logged in!')
            },
        }),
    },
}

export const { POST } = toRouteHandler(router)

and the client component:

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 <UploadDropzone control={control} accept="image/*" />
}

The upup version uses @useupup/next, which bundles the component and the server handlers. Install it first:

sh
npm i @useupup/next

Check your versions first: @useupup/react needs React ^19.0.0, and @useupup/next needs Next.js >=15.0.0 and React ^19.0.0, while Better Upload's client declares only a react peer with no version range. The server handler holds the file rules and the auth check:

ts
// app/api/upup/[...path]/route.ts
import { createUpupNextHandler, defineUpupConfig } from '@useupup/next/server'

export const dynamic = 'force-dynamic'

export const { GET, POST, PUT, DELETE } = createUpupNextHandler(
    defineUpupConfig({
        storage: {
            type: 'aws',
            bucket: process.env.S3_BUCKET!,
            region: process.env.S3_REGION!,
        },
        // Required: at least 16 characters, the same on every instance.
        uploadTokenSecret: process.env.UPUP_UPLOAD_TOKEN_SECRET,
        maxFileSize: 4 * 1024 * 1024,
        allowedTypes: ['image/*'],
        getUserId: async req => {
            const user = await getSessionUser(req) // your session lookup
            return user?.id ?? null
        },
    }),
)

and the client replaces the hook and the copied components:

tsx
'use client'

import { UpupUploader } from '@useupup/next'
import '@useupup/next/styles'

export default function Uploader() {
    return (
        <UpupUploader
            mode="server"
            serverUrl="/api/upup"
            allowedFileTypes="image/*"
            maxFiles={4}
            maxFileSize={{ size: 4, unit: 'MB' }}
            onUploadComplete={files => console.log(files.map(f => f.key))}
        />
    )
}

Add serverExternalPackages: ['@aws-sdk/client-s3', '@aws-sdk/s3-request-presigner'] to your next.config, as the Next.js quickstart describes.

How the options map:

Better Uploadupup
fileTypes on the routeallowedTypes on the handler (415 otherwise); allowedFileTypes on the component for the client-side check
maxFileSize on the route (bytes)maxFileSize on the handler in bytes (413 otherwise); maxFileSize as { size, unit } on the component
maxFiles on the routemaxFiles on the component (client side, default 10)
onBeforeUpload with RejectUploadgetUserId or auth on the handler; hooks.onBeforeUpload returns false to reject with 403
generateObjectInfo returning a keykeyStrategy on the handler
multipart: true, partSizeresumable with protocol: 'multipart' and chunkSizeBytes, in server mode (needs serverUrl)
onUploadCompleteonUploadComplete prop
retrymaxRetries
uploadBatchSizemaxConcurrentUploads (default 3)
upload(files, { metadata })metadata prop
apiserverUrl
UploadDropzone, UploadProgress componentsBuilt into UpupUploader

Two differences to plan for. The <UpupUploader> limits are client-side convenience only; the handler's maxFileSize and allowedTypes are what hold on the server, so set both. And in client mode, where your own endpoint returns presigned URLs and no @useupup/server is involved, multipart is not available; use server mode, or tus. See Server auth for the getUserId and auth options and Resumable uploads for multipart.

When to keep Better Upload

Keep it if you build in React, want to own and restyle the upload components, and only need files to land in your bucket. Its route model keeps the server rules next to the route they belong to, and multipart is one option away. Move to upup when you need the same uploader in Vue, Svelte or Angular, when users pick files from Google Drive or Dropbox, when uploads must survive a dropped connection and resume after a page reload, or when you would rather have a finished interface than assemble one. Keep Better Upload, too, if you are on React 18 or earlier or Next.js 14 or earlier: @useupup/react needs React ^19.0.0, and @useupup/next needs Next.js >=15.0.0 and React ^19.0.0.

FAQ

What is Better Upload?

An MIT-licensed library for uploading files from React directly to an S3-compatible bucket. A server route signs presigned URLs, React hooks upload the files, and shadcn/ui components are copied into your project for the interface.

Which npm package is Better Upload?

Install @better-upload/client and @better-upload/server. The older unscoped better-upload package is deprecated on npm in favor of those two.

Does Better Upload work outside React?

Its server runs in Next.js, Remix, TanStack Start, Hono, Elysia, Express and Fastify. The client hooks and components are React only. upup has native components for React, Vue, Svelte, Angular, Preact and vanilla JavaScript.

Does Better Upload support multipart and resumable uploads?

It supports S3 multipart uploads with one route option, and uploads the parts in parallel. Its documentation does not describe resuming an interrupted upload after a page reload. upup offers S3 multipart through @useupup/server and tus, and both can resume after a reload when the user selects the same file again, or crashRecovery restores it and they click Resume.

How do I migrate from Better Upload to upup?

Replace the route with a createUpupNextHandler or createUpupHandler handler, and the hook and components with one UpupUploader. Set the server limits with maxFileSize and allowedTypes, and move your onBeforeUpload auth check to getUserId.

When should I keep Better Upload?

When you use React, want to own and restyle the upload components, and only need uploads to your bucket. Switching buys little in that case. Also keep it if you are on React 18 or earlier or Next.js 14 or earlier, which upup does not support.