# UploadThing UploadThing is a type-safe file upload system for modern web applications. It provides a unified API for handling file uploads with support for multiple frameworks including Next.js, React, Vue, Solid, Svelte, Express, Fastify, and more. The library offers both server-side file route configuration and client-side upload management with built-in progress tracking, validation, and error handling. The system uses a builder pattern for defining file upload routes with middleware hooks for authentication and authorization, automatic type inference for complete type safety between client and server, and presigned URLs for direct-to-storage uploads. UploadThing handles complex upload scenarios including pause/resume, abort, progress tracking, and custom file identification while providing framework-specific integrations that feel native to each ecosystem. ## Server File Router Configuration Define file upload routes with type-safe validation and middleware ```typescript import { createUploadthing, UTFiles } from "uploadthing/next"; import type { FileRouter } from "uploadthing/next"; import { z } from "zod"; const f = createUploadthing({ errorFormatter: (err) => { console.log("Error uploading file", err.message); return { message: err.message }; }, }); export const uploadRouter = { videoAndImage: f({ image: { maxFileSize: "32MB", maxFileCount: 4, acl: "public-read" }, video: { maxFileSize: "16MB" }, blob: { maxFileSize: "8GB" } }) .input(z.object({ userId: z.string() })) .middleware(async ({ req, files, input }) => { // Authentication check if (!req.headers.get("x-auth-token")) { throw new Error("Unauthorized"); } // Add custom IDs to files const filesWithIds = files.map((file, idx) => ({ ...file, customId: `${input.userId}-${idx}-${Date.now()}`, })); return { userId: input.userId, [UTFiles]: filesWithIds }; }) .onUploadComplete(async ({ file, metadata }) => { console.log("Upload complete:", file.key); console.log("Custom ID:", file.customId); console.log("User:", metadata.userId); return { success: true, fileId: file.key }; }) .onUploadError(async ({ error, fileKey }) => { console.error("Upload failed:", error.message); }) } satisfies FileRouter; export type OurFileRouter = typeof uploadRouter; ``` ## Next.js API Route Handler Create HTTP endpoints for upload handling ```typescript // app/api/uploadthing/route.ts import { createRouteHandler } from "uploadthing/next"; import { uploadRouter } from "~/server/uploadthing"; export const { GET, POST } = createRouteHandler({ router: uploadRouter, config: { logLevel: "Debug", callbackUrl: process.env.UPLOADTHING_CALLBACK_URL, handleDaemonPromise: "await" } }); // GET returns route configuration (JSON metadata) // POST handles upload actions (presigned URL generation, callbacks) ``` ## Express Integration Mount upload routes in Express applications ```typescript import express from "express"; import cors from "cors"; import { createRouteHandler } from "uploadthing/express"; import { uploadRouter } from "./router"; const app = express(); app.use(cors()); app.use( "/api/uploadthing", createRouteHandler({ router: uploadRouter, config: { logLevel: "Info" } }) ); app.listen(3000, () => { console.log("Server listening on port 3000"); }); ``` ## React Hooks Setup Generate type-safe React helpers for client-side uploads ```typescript // utils/uploadthing.ts import { generateReactHelpers, generateUploadButton, generateUploadDropzone, } from "@uploadthing/react"; import type { OurFileRouter } from "~/server/uploadthing"; export const { useUploadThing, uploadFiles } = generateReactHelpers({ url: "/api/uploadthing" }); export const UploadButton = generateUploadButton(); export const UploadDropzone = generateUploadDropzone(); ``` ## React useUploadThing Hook Programmatic file uploads with progress tracking ```typescript "use client"; import { useUploadThing } from "~/utils/uploadthing"; import { useState } from "react"; export default function Uploader() { const [files, setFiles] = useState([]); const [progress, setProgress] = useState(0); const { startUpload, isUploading, routeConfig } = useUploadThing( "videoAndImage", { onBeforeUploadBegin: (files) => { console.log("Uploading", files.length, "files"); // Validate or transform files before upload return files; }, onUploadBegin: (fileName) => { console.log("Starting upload:", fileName); }, onUploadProgress: (p) => { setProgress(p); }, onClientUploadComplete: (res) => { console.log("Uploaded files:", res); res.forEach(file => { console.log("File URL:", file.url); console.log("Server data:", file.serverData); }); }, onUploadError: (error) => { console.error("Upload error:", error.message); }, headers: { "x-auth-token": "your-token" } } ); return (
setFiles(Array.from(e.target.files ?? []))} /> {routeConfig && (

Max file size: {routeConfig.image?.maxFileSize}

)}
); } ``` ## React Upload Components Pre-built UI components for drag-and-drop uploads ```typescript "use client"; import { UploadButton, UploadDropzone } from "~/utils/uploadthing"; export default function UploadPage() { return (
{ console.log("Files uploaded:", res); alert(`Uploaded ${res.length} files`); }} onUploadError={(error) => { alert(`Upload failed: ${error.message}`); }} appearance={{ button: "bg-blue-500 hover:bg-blue-600 text-white", container: "w-max flex-row rounded-md border-cyan-300", allowedContent: "text-gray-500 text-sm" }} content={{ button: "Choose files", allowedContent: "Images up to 32MB, videos up to 16MB" }} config={{ appendOnPaste: true, mode: "manual" }} /> { console.log("Uploaded:", res); }} onUploadAborted={() => { console.log("Upload aborted by user"); }} appearance={{ container: "border-dashed border-2 ut-uploading:border-blue-500", uploadIcon: "text-blue-500", label: "font-semibold text-lg", button: "ut-ready:bg-blue-500" }} />
); } ``` ## Vanilla JavaScript Client Framework-agnostic upload implementation ```typescript import { genUploader, UploadAbortedError } from "uploadthing/client"; import type { OurFileRouter } from "~/server/uploadthing"; const { uploadFiles, routeRegistry } = genUploader({ url: "/api/uploadthing", package: "@uploadthing/vanilla" }); const fileInput = document.querySelector("#file-input"); const progressBar = document.querySelector("#progress"); const abortController = new AbortController(); document.querySelector("form")?.addEventListener("submit", async (e) => { e.preventDefault(); const files = Array.from(fileInput.files || []); try { const res = await uploadFiles("videoAndImage", { files, signal: abortController.signal, input: { userId: "user_123" }, headers: { "x-auth-token": "your-token" }, onUploadProgress: ({ file, progress, totalProgress }) => { console.log(`${file.name}: ${progress}%`); progressBar.value = totalProgress; }, onUploadBegin: ({ file }) => { console.log(`Starting: ${file}`); } }); console.log("Upload complete:", res); res.forEach(file => { console.log("Key:", file.key); console.log("URL:", file.url); console.log("Server data:", file.serverData); }); } catch (error) { if (error instanceof UploadAbortedError) { console.log("Upload aborted"); } else { console.error("Upload failed:", error); } } }); // Abort upload document.querySelector("#abort")?.addEventListener("click", () => { abortController.abort(); }); ``` ## Advanced Upload Control Pause, resume, and manage individual file uploads ```typescript import { genUploader } from "uploadthing/client"; import type { OurFileRouter } from "~/server/uploadthing"; const { createUpload } = genUploader({ url: "/api/uploadthing" }); async function uploadWithControls(files: File[]) { const { pauseUpload, resumeUpload, done } = await createUpload( "videoAndImage", { files, input: { userId: "user_123" }, onUploadProgress: ({ file, progress }) => { console.log(`${file.name}: ${progress}%`); } } ); // Pause specific file document.querySelector("#pause")?.addEventListener("click", () => { pauseUpload(files[0]); }); // Pause all uploads document.querySelector("#pause-all")?.addEventListener("click", () => { pauseUpload(); }); // Resume specific file document.querySelector("#resume")?.addEventListener("click", () => { resumeUpload(files[0]); }); // Resume all uploads document.querySelector("#resume-all")?.addEventListener("click", () => { resumeUpload(); }); // Wait for specific file const firstResult = await done(files[0]); console.log("First file done:", firstResult); // Wait for all files const allResults = await done(); console.log("All files done:", allResults); } ``` ## Server SDK - UTApi Server-side file management operations ```typescript import { UTApi } from "uploadthing/server"; const utapi = new UTApi({ fetch: customFetch, defaultKeyType: "fileKey" }); // Upload files from server const uploadResult = await utapi.uploadFiles( [new File(["content"], "file.txt")], { concurrency: 5, metadata: { userId: "user_123" }, contentDisposition: "inline" } ); console.log("Uploaded:", uploadResult); // { data: { key: "...", url: "...", ... }, error: null } // Upload from URL const fromUrl = await utapi.uploadFilesFromUrl([ "https://example.com/image.jpg", { url: "https://example.com/video.mp4", name: "custom-name.mp4" } ], { concurrency: 3 }); // Delete files await utapi.deleteFiles(["file-key-1", "file-key-2"], { keyType: "fileKey" }); // Get file URLs const urls = await utapi.getFileUrls(["file-key-1", "file-key-2"]); console.log(urls); // [{ key: "...", url: "..." }, ...] // List files with pagination const files = await utapi.listFiles({ limit: 100, offset: 0 }); console.log("Total:", files.count); console.log("Files:", files.files); // Rename files await utapi.renameFiles([ { fileKey: "old-key", newName: "new-name.jpg" } ]); // Get usage info const usage = await utapi.getUsageInfo(); console.log("Total bytes:", usage.totalBytes); console.log("Files:", usage.filesUploaded); // Generate signed URL const signedUrl = await utapi.generateSignedURL("file-key", { expiresIn: "5 minutes" }); console.log("Signed URL:", signedUrl); // Update ACL (access control) await utapi.updateACL( ["file-key-1", "file-key-2"], "private", { keyType: "fileKey" } ); ``` ## Vue 3 Integration Type-safe file uploads in Vue applications ```typescript // composables/uploadthing.ts import { generateVueHelpers } from "@uploadthing/vue"; import type { OurFileRouter } from "~/server/uploadthing"; export const { useUploadThing, uploadFiles } = generateVueHelpers({ url: "/api/uploadthing" }); // Component usage ``` ## Solid JS Integration Reactive file uploads for Solid applications ```typescript // utils/uploadthing.ts import { generateSolidHelpers } from "@uploadthing/solid"; import type { OurFileRouter } from "~/server/uploadthing"; export const { useUploadThing, uploadFiles } = generateSolidHelpers({ url: "/api/uploadthing" }); // Component usage import { createSignal } from "solid-js"; import { useUploadThing } from "~/utils/uploadthing"; export default function Uploader() { const [files, setFiles] = createSignal([]); const [progress, setProgress] = createSignal(0); const { startUpload, isUploading, routeConfig } = useUploadThing( "videoAndImage", { onUploadProgress: (p) => setProgress(p), onClientUploadComplete: (res) => { console.log("Uploaded:", res); }, onUploadError: (error) => { console.error("Error:", error.message); } } ); return (
setFiles(Array.from(e.target.files || []))} /> {routeConfig() && (

Max size: {routeConfig()!.image?.maxFileSize}

)}
); } ``` ## File Validation Client-side validation using route configuration ```typescript import { isValidFileType, isValidFileSize, generateMimeTypes, generatePermittedFileTypes } from "uploadthing/client"; import type { ExpandedRouteConfig } from "@uploadthing/shared"; const routeConfig: ExpandedRouteConfig = { image: { maxFileSize: "4MB", maxFileCount: 3, minFileCount: 1 }, video: { maxFileSize: "16MB", maxFileCount: 1 } }; // Validate file type const file = new File(["content"], "image.jpg", { type: "image/jpeg" }); if (!isValidFileType(file, routeConfig)) { console.error("Invalid file type"); } // Validate file size if (!isValidFileSize(file, routeConfig)) { console.error("File too large"); } // Generate MIME types for accept attribute const mimeTypes = generateMimeTypes(routeConfig); console.log(mimeTypes); // ["image/*", "video/*"] // Generate permitted file types const permitted = generatePermittedFileTypes(routeConfig); console.log(permitted); // { image: [".jpg", ".jpeg", ".png", ...], video: [".mp4", ".mov", ...] } // Use in HTML const accept = mimeTypes.join(","); // ``` ## Error Handling Comprehensive error handling with typed errors ```typescript import { UploadThingError, UploadAbortedError } from "@uploadthing/shared"; try { const res = await uploadFiles("videoAndImage", { files }); } catch (error) { if (error instanceof UploadAbortedError) { console.log("User cancelled upload"); } else if (error instanceof UploadThingError) { console.error("Code:", error.code); console.error("Message:", error.message); switch (error.code) { case "BAD_REQUEST": console.error("Invalid request"); break; case "TOO_LARGE": console.error("File exceeds size limit"); break; case "TOO_MANY_FILES": console.error("Too many files selected"); break; case "FORBIDDEN": console.error("Unauthorized"); break; case "UPLOAD_FAILED": console.error("Upload failed:", error.cause); break; default: console.error("Unknown error"); } } else { console.error("Unexpected error:", error); } } // Custom error formatting in file router const f = createUploadthing({ errorFormatter: (err) => { console.error("Full error:", err); return { message: err.message, code: err.code, timestamp: Date.now() }; } }); ``` ## Region Selection and Custom IDs Advanced middleware configuration ```typescript import { createUploadthing, UTFiles, UTRegion } from "uploadthing/next"; import { nanoid } from "nanoid"; const f = createUploadthing(); export const uploadRouter = { regionalUpload: f({ image: { maxFileSize: "4MB" } }) .middleware(async ({ req, files }) => { const user = await authenticateUser(req); const userCountry = req.headers.get("cf-ipcountry"); // Select region based on user location const region = userCountry === "JP" ? "icn1" : "sfo1"; // Add custom IDs and metadata to files const filesWithMetadata = files.map((file) => ({ ...file, customId: `${user.id}-${nanoid()}` })); return { userId: user.id, uploadedAt: Date.now(), [UTRegion]: region, [UTFiles]: filesWithMetadata }; }) .onUploadComplete(async ({ file, metadata }) => { console.log("Region:", file.region); console.log("Custom ID:", file.customId); console.log("User:", metadata.userId); console.log("Timestamp:", metadata.uploadedAt); // Store in database await db.files.create({ id: file.customId, key: file.key, userId: metadata.userId, uploadedAt: metadata.uploadedAt }); return { fileId: file.customId }; }) }; ``` UploadThing provides a complete solution for file uploads in modern web applications with type safety, flexible configuration, and framework-specific integrations. The server-side file router pattern allows defining upload constraints, validation rules, and business logic with middleware hooks for authentication and authorization. Client-side helpers are generated from the server router definition, ensuring perfect type safety between client and server with no manual type definitions required. The library supports multiple upload strategies including simple fire-and-forget uploads, resumable uploads with pause/resume capabilities, and abort functionality. Progress tracking is granular with per-file and aggregate progress reporting, while error handling provides detailed error codes and custom error formatting. The server SDK enables direct file management operations including uploads, deletions, URL generation, ACL management, and usage analytics without going through the client upload flow.