Introduction
UploadThing is an open-source file upload solution specifically designed for Next.js applications. It provides developers with a type-safe, efficient way to handle file uploads while offering features like file validation, transformation, and direct integration with popular frameworks.
Technical Overview
At its core, UploadThing consists of three main components:
- Server-side file router
- Client-side components and hooks
- Type-safe API endpoints
Installation and Basic Setup
First, install the required packages:
npm install uploadthing @uploadthing/react
Create a file router (typically in app/api/uploadthing/core.ts
):
import { createUploadthing, type FileRouter } from "uploadthing/server";
const f = createUploadthing();
export const uploadRouter = {
// Example "profile picture upload" route - these can be named whatever you want!
profilePicture: f(["image"])
.middleware(({ req }) => auth(req))
.onUploadComplete((data) => console.log("file", data)),
// This route takes an attached image OR video
messageAttachment: f(["image", "video"])
.middleware(({ req }) => auth(req))
.onUploadComplete((data) => console.log("file", data)),
// Takes exactly ONE image up to 2MB
strictImageAttachment: f({
image: { maxFileSize: "2MB", maxFileCount: 1, minFileCount: 1 },
})
.middleware(({ req }) => auth(req))
.onUploadComplete((data) => console.log("file", data)),
// Takes up to 4 2mb images and/or 1 256mb video
mediaPost: f({
image: { maxFileSize: "2MB", maxFileCount: 4 },
video: { maxFileSize: "256MB", maxFileCount: 1 },
})
.middleware(({ req }) => auth(req))
.onUploadComplete((data) => console.log("file", data)),
// Takes up to 4 2mb images, and the client will not resolve
// the upload until the `onUploadComplete` resolved.
withAwaitedServerData: f(
{ image: { maxFileSize: "2MB", maxFileCount: 4 } },
{ awaitServerData: true },
)
.middleware(({ req }) => auth(req))
.onUploadComplete((data) => {
return { foo: "bar" as const };
}),
} satisfies FileRouter;
export type UploadRouter = typeof uploadRouter;
These are the routes you create with the helper instantiated by createUploadthing
. Think of them as the "endpoints" for what your users can upload. An object with file routes constructs a file router where the keys (slugs) in the object are the names of your endpoints.
Route Config:
The f
function takes two arguments. The first can be an array of FileType
, or a record mapping each FileType
with a route config. The route config allow more granular control, for example what files can be uploaded and how many of them can be uploaded for a given upload. The array syntax will fallback to applying the defaults to all file types.
A FileType
can be any valid web MIME type ↗. For example: use application/json
to only allow JSON files to be uploaded.
Additionally, you may pass any of the following custom types: image
, video
, audio
, pdf
or text
. These are shorthands that allows you to specify the type of file without specifying the exact MIME type. Lastly, there's blob
which allows any file type.
Route Options:
The second argument to the f
function is an optional object of route options. These configurations provide global settings that affect how the upload route behaves.
Available route options:
-
awaitServerData: (boolean, default: false)
When set to
true
, the client will wait for the server'sonUploadComplete
handler to finish and return data before runningonClientUploadComplete
. This is useful when you need to ensure server-side processing is complete before proceeding with client-side operations.
Example:
const uploadRouter = {
withServerData: f(
{ image: { maxFileSize: "2MB" } },
{ awaitServerData: true }
)
.middleware(({ req }) => ({ userId: "123" }))
.onUploadComplete(async ({ metadata }) => {
// This will complete before client-side callback
const result = await processImage(metadata);
return { processedUrl: result.url };
}),
}
Route Methods
The f
function returns a builder object that allows you to chain the following methods:
input
Validates user input from the client using schema validators. This method ensures that any additional data sent alongside the file upload meets your specifications.
Supported validators:
- Zod (≥3)
- Effect/Schema (≥3.10, with limitations)
- Standard Schema specification (e.g., Valibot ≥1.0, ArkType ≥2.0)
Example with complex validation:
f(["image"])
.input(
z.object({
title: z.string().min(1).max(100),
tags: z.array(z.string()),
isPublic: z.boolean()
})
)
.middleware(async ({ req, input }) => {
// input is fully typed with:
// { title: string; tags: string[]; isPublic: boolean }
return { metadata: input };
})
middleware
Handles authorization and metadata tagging. This is where you perform authentication checks and prepare any data needed for the upload process.
Example with comprehensive auth and metadata:
f(["image"])
.middleware(async ({ req, res }) => {
const user = await currentUser();
if (!user) throw new UploadThingError("Authentication required");
// You can perform additional checks
const userPlan = await getUserSubscriptionPlan(user.id);
if (userPlan === "free" && await getUserUploadCount(user.id) > 10) {
throw new UploadThingError("Free plan limit reached");
}
return {
userId: user.id,
planType: userPlan,
timestamp: new Date().toISOString()
};
})
onUploadError
Called when an upload error occurs. Use this to handle errors gracefully and perform any necessary cleanup or logging.
Parameters:
-
error
: UploadThingError (contains error message and code) -
fileKey
: string (unique identifier for the failed upload)
Example:
f(["image"])
.onUploadError(async ({ error, fileKey }) => {
await logger.error("Upload failed", {
error: error.message,
fileKey,
code: error.code
});
await cleanupFailedUpload(fileKey);
})
onUploadComplete
Final handler for successful uploads. This is where you can process the uploaded file and perform any necessary post-upload operations.
Parameters:
-
metadata
: Data passed from middleware -
file
: UploadedFileData object containing:-
name
: Original file name -
size
: File size in bytes -
key
: Unique file identifier -
url
: Public URL of the uploaded file
-
Example with comprehensive handling:
f(["image"])
.onUploadComplete(async ({ metadata, file }) => {
// Store file reference in database
await db.files.create({
data: {
userId: metadata.userId,
fileName: file.name,
fileSize: file.size,
fileUrl: file.url,
uploadedAt: metadata.timestamp
}
});
// Trigger any post-upload processing
await imageProcessor.optimize(file.url);
// Return data to client if awaitServerData is true
return {
fileId: file.key,
accessUrl: file.url,
processedAt: new Date().toISOString()
};
})
Uploading Files
UploadThing provides two primary methods for uploading files: Client-Side Uploads and Server-Side Uploads. Each approach has its own benefits and use cases.
Client-Side Uploads
Client-side uploads are the recommended approach as they offer several advantages:
- Reduced server costs (no ingress/egress fees)
- Direct file transfer to UploadThing
- Built-in validation and type safety
The process works as follows:
- Client initiates upload request
- Server generates presigned URLs
- Client uploads directly to UploadThing
- Server receives callback on completion
Example implementation using React:
import { UploadButton } from "@uploadthing/react";
export default function UploadPage() {
return (
<UploadButton
endpoint="imageUploader"
onClientUploadComplete={(res) => {
console.log("Files: ", res);
alert("Upload Completed");
}}
onUploadError={(error: Error) => {
alert(`ERROR! ${error.message}`);
}}
/>
);
}
Server-Side Uploads
Server-side uploads are useful when you need to:
- Process or validate files before uploading
- Generate files on the server
- Handle specific security requirements
Example using the UTApi:
import { UTApi } from "uploadthing/server";
const utapi = new UTApi();
async function uploadServerSideFile(file: File) {
try {
const response = await utapi.uploadFiles(file);
return response.data;
} catch (error) {
console.error("Upload failed:", error);
throw error;
}
}
Upload Configuration
Both client and server uploads support various configuration options:
File Validation:
const uploadRouter = {
strictImageUpload: f({
image: {
maxFileSize: "4MB",
maxFileCount: 1,
minFileCount: 1
}
})
}
Custom Metadata:
f(["image"])
.middleware(({ req }) => ({
userId: req.userId,
uploadedAt: new Date().toISOString()
}))
Upload Callbacks:
f(["image"])
.onUploadComplete(async ({ metadata, file }) => {
// Handle successful upload
await db.images.create({
data: {
userId: metadata.userId,
url: file.url,
name: file.name
}
});
})
.onUploadError(({ error }) => {
// Handle upload error
console.error("Upload failed:", error);
});
Resumable Uploads
UploadThing supports resumable uploads for large files:
const upload = async (file: File, presignedUrl: string) => {
// Get the current range start
const rangeStart = await fetch(presignedUrl, {
method: "HEAD"
}).then((res) =>
parseInt(res.headers.get("x-ut-range-start") ?? "0", 10)
);
// Continue upload from last successful byte
await fetch(presignedUrl, {
method: "PUT",
headers: {
Range: `bytes=${rangeStart}-`,
},
body: file.slice(rangeStart),
});
};
Security Considerations
- URL Signing: All upload URLs are signed with your API key and include expiration timestamps
- File Validation: Implement thorough validation in your file routes
- Authentication: Always use middleware to authenticate users before allowing uploads
Example secure configuration:
const uploadRouter = {
secureUpload: f(["image"])
.middleware(async ({ req }) => {
const user = await auth(req);
if (!user) throw new Error("Unauthorized");
const userQuota = await checkUserQuota(user.id);
if (!userQuota.hasRemaining) {
throw new Error("Upload quota exceeded");
}
return { userId: user.id };
})
.onUploadComplete(async ({ metadata, file }) => {
await saveToDatabase(metadata.userId, file);
await updateUserQuota(metadata.userId);
})
};
Working with Files
After successfully uploading files to UploadThing, there are several ways to work with and access these files. Here's a comprehensive guide on file operations.
Accessing Public Files
Files are served through UploadThing's CDN using the following URL pattern:
https://<APP_ID>.ufs.sh/f/<FILE_KEY>
If you've set a customId
during upload, you can also access files using:
https://<APP_ID>.ufs.sh/f/<CUSTOM_ID>
Important: Never use raw storage provider URLs (e.g.,
https://bucket.s3.region.amazonaws.com/<FILE_KEY>
). UploadThing may change storage providers or buckets, making these URLs unreliable.
Setting Up Image Optimization (Next.js Example)
/** @type {import('next').NextConfig} */
export default {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "<APP_ID>.ufs.sh",
pathname: "/f/*",
},
],
},
};
UTApi Reference
The UploadThing API Helper is designed for server-side use. While it provides a REST API interface, it offers enhanced functionality and type safety.
Note: External API calls will typically be slower than querying your own database. It's recommended to store necessary file data in your database, either in
onUploadComplete()
or after usinguploadFiles()
, rather than relying on the API for core data flow.
Constructor
Initialize an instance of UTApi
:
import { UTApi } from "uploadthing/server";
export const utapi = new UTApi({
// ...options
});
Configuration Options:
- fetch: Custom fetch function
- token: Your UploadThing token (default: env.UPLOADTHING_TOKEN)
- logLevel: Logging verbosity (Error | Warning | Info | Debug | Trace)
- logFormat: Log format (json | logFmt | structured | pretty)
- defaultKeyType: Default key type for file operations ('fileKey' | 'customId')
- apiUrl: UploadThing API URL (default: https://api.uploadthing.com)
- ingestUrl: UploadThing Ingest API URL
File Operations
Upload Files
import { utapi } from "~/server/uploadthing";
async function uploadFiles(formData: FormData) {
"use server";
const files = formData.getAll("files");
const response = await utapi.uploadFiles(files);
}
Upload Files from URL
const fileUrl = "https://test.com/some.png";
const uploadedFile = await utapi.uploadFilesFromUrl(fileUrl);
const fileUrls = ["https://test.com/some.png", "https://test.com/some2.png"];
const uploadedFiles = await utapi.uploadFilesFromUrl(fileUrls);
Delete Files
await utapi.deleteFiles("2e0fdb64-9957-4262-8e45-f372ba903ac8_image.jpg");
await utapi.deleteFiles([
"2e0fdb64-9957-4262-8e45-f372ba903ac8_image.jpg",
"1649353b-04ea-48a2-9db7-31de7f562c8d_image2.jpg",
]);
List Files
const files = await utapi.listFiles({
limit: 500, // optional, default: 500
offset: 0 // optional, default: 0
});
Rename Files
await utapi.renameFiles({
key: "2e0fdb64-9957-4262-8e45-f372ba903ac8_image.jpg",
newName: "myImage.jpg",
});
// Batch rename
await utapi.renameFiles([
{
key: "2e0fdb64-9957-4262-8e45-f372ba903ac8_image.jpg",
newName: "myImage.jpg",
},
{
key: "1649353b-04ea-48a2-9db7-31de7f562c8d_image2.jpg",
newName: "myOtherImage.jpg",
},
]);
Get Signed URL
const fileKey = "2e0fdb64-9957-4262-8e45-f372ba903ac8_image.jpg";
const url = await utapi.getSignedURL(fileKey, {
expiresIn: 60 * 60, // 1 hour
// or use time strings:
// expiresIn: '1 hour',
// expiresIn: '3d',
// expiresIn: '7 days',
});
Update ACL
// Make a single file public
await utapi.updateACL(
"2e0fdb64-9957-4262-8e45-f372ba903ac8_image.jpg",
"public-read"
);
// Make multiple files private
await utapi.updateACL(
[
"2e0fdb64-9957-4262-8e45-f372ba903ac8_image.jpg",
"1649353b-04ea-48a2-9db7-31de7f562c8d_image2.jpg",
],
"private"
);
Accessing Private Files
For files protected by access controls, you'll need to generate short-lived presigned URLs. There are two ways to do this:
Using UTApi:
import { UTApi } from "uploadthing/server";
const utapi = new UTApi();
async function getFileAccess(fileKey: string) {
const signedUrl = await utapi.getSignedUrl(fileKey, {
expiresIn: "1h" // Optional expiration time
});
return signedUrl;
}
Using REST API Endpoint:
async function requestFileAccess(fileKey: string) {
const response = await fetch("/api/requestFileAccess", {
method: "POST",
body: JSON.stringify({ fileKey })
});
const { signedUrl } = await response.json();
return signedUrl;
}
Best Practices
URL Management:
- Always use the CDN URLs provided by UploadThing
- Store file keys rather than full URLs in your database
- Generate presigned URLs on-demand for private files
Security:
- Implement proper access controls in your middleware
- Use short expiration times for presigned URLs
- Validate file access permissions before generating signed URLs
Performance:
- Utilize the CDN for optimal file delivery
- Consider implementing caching for frequently accessed files
- Use appropriate image optimization settings
Example implementation combining these practices:
const fileManager = {
async getFileUrl(fileKey: string, userId: string) {
// Check user permissions
const hasAccess = await checkUserFileAccess(userId, fileKey);
if (!hasAccess) {
throw new Error("Unauthorized access");
}
// Get cached URL if available
const cachedUrl = await cache.get(`file:${fileKey}`);
if (cachedUrl) return cachedUrl;
// Generate new signed URL
const signedUrl = await utapi.getSignedUrl(fileKey, {
expiresIn: "1h"
});
// Cache the URL (for slightly less than expiration time)
await cache.set(`file:${fileKey}`, signedUrl, 50 * 60); // 50 minutes
return signedUrl;
},
async deleteUserFile(fileKey: string, userId: string) {
// Verify ownership
const isOwner = await verifyFileOwnership(userId, fileKey);
if (!isOwner) {
throw new Error("Unauthorized deletion");
}
// Delete file
await utapi.deleteFiles(fileKey);
// Clean up database records
await db.files.delete({
where: { fileKey }
});
// Clear cache
await cache.del(`file:${fileKey}`);
}
};
Conclusion
UploadThing provides a robust, type-safe solution for handling file uploads in Next.js applications. Its key strengths include:
- Developer Experience: Type-safe APIs and intuitive integration with React components
- Flexibility: Support for both client and server-side uploads with customizable workflows
- Security: Built-in file validation, access controls, and secure URL signing
- Performance: CDN-backed delivery and resumable uploads for large files
Whether you're building a simple image upload feature or a complex file management system, UploadThing offers the tools and flexibility needed to implement secure and efficient file handling in your applications.
For more information and updates, visit the official UploadThing documentation.
Top comments (0)