Handling File Uploads in Next.js Using UploadThing

A comprehensive guide to implementing robust, type-safe file uploads with UploadThing in Next.js applications. Learn client-side and server-side patterns, security best practices, and performance optimization strategies.

Why UploadThing for Next.js

Traditional file upload implementations require developers to configure cloud storage buckets, generate presigned URLs, manage upload signatures, and handle the complex multipart form parsing that varies between server environments. UploadThing abstracts these concerns entirely, providing a unified API that works consistently across client-side and server-side contexts.

The Modern Approach to File Uploads

UploadThing handles file validation at the edge before any data reaches your servers, reducing unnecessary bandwidth costs and protecting your infrastructure from malicious uploads. The type-safe architecture means your file upload routes are fully typed from the file router through to your React components.

Key Benefits

  • Direct-to-CDN uploads reduce server load and improve upload speeds
  • Built-in file validation eliminates custom validation code
  • Type-safe APIs prevent runtime errors from misconfiguration
  • Automatic image optimization and transformation
  • Consistent behavior across all Next.js rendering modes

UploadThing Documentation provides comprehensive coverage of these capabilities.

Performance Considerations

Files upload directly to UploadThing's global CDN, bypassing your application servers. This reduces latency for users regardless of their geographic location and maintains optimal Time to First Byte (TTFB) for dynamic content. Image transformations happen at the edge, reducing origin server load.

For teams building modern web applications, leveraging specialized tools like UploadThing allows developers to focus on business logic rather than infrastructure complexity.

UploadThing Core Capabilities

Everything you need for production-ready file uploads

Type-Safe File Router

Define upload routes with full TypeScript inference. Configuration constraints flow through your entire application, catching errors at compile time.

Client-Side Components

Pre-built React components handle the entire upload flow, including progress tracking, drag-and-drop, and multi-file uploads.

Server-Side UTApi

Programmatic file operations for non-browser uploads, URL-based uploads, and complete file management capabilities.

Edge-Based Validation

Files are validated at UploadThing's edge before reaching your servers, reducing bandwidth costs and improving security.

Resumable Uploads

Large file uploads support resuming from the last successful byte, essential for unreliable connections.

Global CDN Delivery

Files are served from edge locations worldwide, ensuring fast delivery regardless of user location.

Setting Up UploadThing

Installation

npm install uploadthing @uploadthing/react

The core package provides server-side functionality for defining file routes and handling uploads, while the React package includes components and hooks for implementing upload interfaces in your frontend components.

Environment Configuration

UPLOADTHING_SECRET=sk_live_...
UPLOADTHING_APP_ID=your-app-id

The secret key handles server-side authentication for generating upload URLs and managing files, while the app ID identifies your application in UploadThing's infrastructure. Never expose the secret key to client-side code.

Creating a File Router

import { createUploadthing, type FileRouter } from "uploadthing/server";

const f = createUploadthing();

export const uploadRouter = {
 profilePicture: f(["image", "video"])
 .middleware(({ req }) => {
 return { userId: "user_123" };
 })
 .onUploadComplete(({ metadata, file }) => {
 console.log("File uploaded:", file.url);
 return { fileId: file.key };
 }),
} satisfies FileRouter;

Each route is defined as a method on the router object, specifying the file types it accepts, any middleware for authentication or validation, and callbacks for handling upload completion.

UploadThing Documentation covers all file router configuration options in detail.

File Validation and Configuration

Understanding File Types

UploadThing supports multiple approaches to specifying accepted file types:

// Specific MIME types
documentUpload: f(["application/pdf", "application/msword"]),

// Predefined categories with detailed config
mediaUpload: f({
 image: { maxFileSize: "4MB", maxFileCount: 4 },
 video: { maxFileSize: "256MB", maxFileCount: 1 },
 audio: { maxFileSize: "32MB", maxFileCount: 10 },
}),

// Accept any file type
backupUpload: f("blob"),

Route Options

const uploadRouter = {
 processingUpload: f(
 { image: { maxFileSize: "2MB", maxFileCount: 4 } },
 { awaitServerData: true }
 )
 .middleware(async ({ req }) => {
 return { userId: "user_123" };
 })
 .onUploadComplete(async ({ metadata, file }) => {
 const processed = await processImage(file.url);
 return { processedUrl: processed.url };
 }),
} satisfies FileRouter;

The awaitServerData option changes the default upload flow. When enabled, the client waits for the server's onUploadComplete handler to finish before executing client-side callbacks.

Input Validation

import { z } from "zod";

const uploadRouter = {
 contentUpload: f(["image", "video"])
 .input(
 z.object({
 title: z.string().min(1).max(100),
 description: z.string().max(500).optional(),
 tags: z.array(z.string()),
 isPublic: z.boolean(),
 })
 )
 .middleware(async ({ req, input }) => {
 // Input is fully typed here
 return { metadata: input };
 }),
} satisfies FileRouter;

CodeParrot provides detailed examples of the awaitServerData pattern and its practical applications.

Client-Side Uploads

Using UploadButton

"use client";

import { UploadButton } from "@uploadthing/react";

export default function ProfilePage() {
 return (
 <UploadButton
 endpoint="profilePicture"
 onClientUploadComplete={(res) => {
 console.log("Files:", res);
 alert("Upload Completed");
 }}
 onUploadError={(error) => {
 alert(`ERROR! ${error.message}`);
 }}
 onUploadBegin={(file) => {
 console.log("Uploading:", file.name);
 }}
 />
 );
}

Custom Upload Components

"use client";

import { useUploadThing } from "@uploadthing/react";

export function Dropzone() {
 const { startUpload, isUploading, uploadProgress } = useUploadThing(
 "mediaPost",
 {
 onClientUploadComplete: (res) => console.log("Upload complete:", res),
 onUploadError: (error) => console.error("Upload failed:", error),
 }
 );

 const handleDrop = async (e: React.DragEvent) => {
 e.preventDefault();
 const files = Array.from(e.dataTransfer.files);
 if (files.length > 0) {
 await startUpload(files);
 }
 };

 return (
 <div className="dropzone" onDrop={handleDrop} onDragOver={(e) => e.preventDefault()}>
 {isUploading ? (
 <div className="progress">Uploading... {uploadProgress}%</div>
 ) : (
 <p>Drop files here to upload</p>
 )}
 </div>
 );
}

LogRocket offers additional React component patterns and best practices for production implementations.

Server-Side Uploads

The UTApi Class

import { UTApi } from "uploadthing/server";

const utapi = new UTApi();

async function uploadServerFile(file: File) {
 const response = await utapi.uploadFiles(file);
 return response.data;
}

Uploading from URLs

// Upload directly from external URLs
const uploadedFile = await utapi.uploadFilesFromUrl(url);

// Batch upload from URLs
const uploadedFiles = await utapi.uploadFilesFromUrl(urls);

File Management Operations

// List files
const files = await utapi.listFiles({ limit: 100, offset: 0 });

// Delete files
await utapi.deleteFiles("file-key-image.jpg");
await utapi.deleteFiles(["file1.jpg", "file2.jpg"]);

// Rename files
await utapi.renameFiles({ key: "old-name.jpg", newName: "new-name.jpg" });

// Generate signed URLs for private files
const signedUrl = await utapi.getSignedURL("file-key.jpg", {
 expiresIn: 60 * 60,
});

UploadThing Documentation provides the complete UTApi reference with all available operations and options.

Server-side file management pairs well with AI automation workflows for processing uploaded content, such as image analysis, document parsing, or automated categorization.

Security Best Practices

Securing Upload Endpoints

const secureUploadRouter = {
 protectedUpload: f(["image", "video"])
 .middleware(async ({ req }) => {
 const session = await getSession(req);
 
 if (!session) {
 throw new UploadThingError("Unauthorized", {
 code: "UNAUTHORIZED",
 });
 }

 const canUpload = await checkUserPermission(session.userId);
 if (!canUpload) {
 throw new UploadThingError("Permission denied", {
 code: "FORBIDDEN",
 });
 }

 return { userId: session.userId };
 })
 .onUploadComplete(async ({ metadata, file }) => {
 await db.files.create({
 data: {
 userId: metadata.userId,
 fileUrl: file.url,
 fileKey: file.key,
 accessLevel: "private",
 },
 });
 }),
} satisfies FileRouter;

Handling Upload Errors

const uploadRouter = {
 monitoredUpload: f(["image"])
 .onUploadError(async ({ error, fileKey }) => {
 await logger.error("Upload failed", {
 error: error.message,
 code: error.code,
 fileKey,
 });
 
 if (fileKey) {
 await cleanupFailedUpload(fileKey);
 }
 })
 .onUploadComplete(async ({ metadata, file }) => {
 // Successful upload handling
 }),
} satisfies FileRouter;

Presigned URL Security

const signedUrl = await utapi.getSignedURL(fileKey, {
 expiresIn: 60 * 15, // 15 minutes
});

Short expiration times limit the window during which URLs can be misused. When implementing file upload systems, always follow security best practices covered in our web development services to ensure your applications remain secure.

Performance Optimization

Resumable Uploads for Large Files

async function uploadWithResume(file: File, presignedUrl: string) {
 const rangeStart = await fetch(presignedUrl, { 
 method: "HEAD" 
 }).then(res =>
 parseInt(res.headers.get("x-ut-range-start") || "0", 10)
 );

 if (rangeStart > 0) {
 console.log(`Resuming upload from byte ${rangeStart}`);
 }

 const response = await fetch(presignedUrl, {
 method: "PUT",
 headers: {
 Range: `bytes=${rangeStart}-`,
 },
 body: file.slice(rangeStart),
 });

 if (response.ok) {
 console.log("Upload completed successfully");
 }
}

CDN-Based Delivery Configuration

/** @type {import('next').NextConfig} */
const nextConfig = {
 images: {
 remotePatterns: [
 {
 protocol: "https",
 hostname: "<APP_ID>.ufs.sh",
 pathname: "/f/*",
 },
 ],
 },
};

Performance Best Practices

  • Always use CDN URLs rather than raw storage URLs
  • Store file keys rather than complete URLs in your database
  • Generate presigned URLs on-demand for private files
  • Implement caching for frequently accessed signed URLs
  • Use appropriate image optimization settings

CodeParrot covers URL management best practices and performance optimization strategies in detail.

Common Implementation Patterns

Multi-Upload with Preview

"use client";

import { UploadButton, useUploadThing } from "@uploadthing/react";

export function MultiUploadWithPreview() {
 const { startUpload, isUploading } = useUploadThing(
 "imageUploader",
 {
 onClientUploadComplete: (uploadedFiles) => {
 setPreviews(uploadedFiles.map(f => f.url));
 },
 }
 );

 return (
 <div className="upload-section">
 <UploadButton
 endpoint="imageUploader"
 onUploadComplete={startUpload}
 multiple
 />
 
 {isUploading && <ProgressBar />}
 
 <div className="preview-grid">
 {previews.map((url, index) => (
 <img key={index} src={url} alt={`Preview ${index}`} />
 ))}
 </div>
 </div>
 );
}

Profile Avatar Upload

const profileRouter = {
 avatar: f(["image"])
 .middleware(async ({ req }) => {
 const session = await getSession(req);
 if (!session) throw new UploadThingError("Unauthorized");
 return { userId: session.userId };
 })
 .onUploadComplete(async ({ metadata, file }) => {
 if (oldUser?.avatarKey) {
 await utapi.deleteFiles(oldUser.avatarKey);
 }
 
 await db.user.update({
 where: { id: metadata.userId },
 data: {
 avatarUrl: file.url,
 avatarKey: file.key,
 },
 });
 }),
} satisfies FileRouter;

Troubleshooting Common Issues

Authentication Errors

Authentication failures typically stem from missing or expired session data:

  • Ensure cookies are properly configured in your Next.js application
  • Verify API keys are set in environment variables
  • Check that middleware correctly validates tokens

File Size Limits

When uploads fail with size-related errors:

  • Confirm the route configuration allows the file type and size
  • Check browser network tab for actual upload size
  • Verify no intermediate proxies impose limits

CORS Issues

Cross-origin errors occur when the UploadThing domain isn't properly configured:

  • Ensure your app ID is correctly specified in configuration
  • Check browser console for specific CORS error messages
  • Verify no browser extensions are blocking requests

Conclusion

UploadThing provides a comprehensive solution for file uploads in Next.js applications, combining type safety, performance, and developer experience into a single platform. By handling the complexities of file storage, validation, and delivery, it allows developers to focus on building application features rather than infrastructure.

Key takeaways:

  • Type safety from file router to React components
  • Performance through direct-to-CDN uploads and edge validation
  • Developer experience with pre-built components and hooks
  • Security built into every upload with middleware and signed URLs
  • Flexibility for both client-side and server-side uploads

Whether implementing simple profile picture uploads or complex document management systems, UploadThing provides the tools for building robust, performant file upload experiences.

Need help implementing file uploads or other modern web development features? Our web development team specializes in Next.js applications and can help you build scalable, secure file management systems.

Frequently Asked Questions

Ready to Build Modern Web Applications?

Our team specializes in Next.js development with modern tools like UploadThing. Let's discuss how we can help you implement robust file upload solutions.