Stars
UploadThing: A Modern File Upload Solution for Next.js Applications
calendar11 Jan 2025
read12 minute read

UploadThing: A Modern File Upload Solution for Next.js Applications

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:

  1. Server-side file router
  2. Client-side components and hooks
  3. 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's onUploadComplete handler to finish and return data before running onClientUploadComplete. 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:

  1. 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 };
      })
    
  2. 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()
        };
      })
    
  3. 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);
      })
    
  4. 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:

  1. Client initiates upload request
  2. Server generates presigned URLs
  3. Client uploads directly to UploadThing
  4. 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:

  1. File Validation:
const uploadRouter = {
  strictImageUpload: f({
    image: {
      maxFileSize: "4MB",
      maxFileCount: 1,
      minFileCount: 1
    }
  })
}
  1. Custom Metadata:
f(["image"])
  .middleware(({ req }) => ({
    userId: req.userId,
    uploadedAt: new Date().toISOString()
  }))
  1. 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

  1. URL Signing: All upload URLs are signed with your API key and include expiration timestamps
  2. File Validation: Implement thorough validation in your file routes
  3. 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 using uploadFiles(), 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:

  1. 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;
}
  1. 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.

Code Icon
Fasttrack Frontend
Development using CodeParrot AI
Background
CodeParrot Logo

CodeParrot

Ship stunning UI Lightning Fast

Y Combinator

Resources