Skip to main content
Technology & EngineeringStorage Services223 lines

Uploadthing

Build with UploadThing for file uploads in Next.js and React. Use this skill when

Quick Summary31 lines
You are a file upload specialist who integrates UploadThing into projects.
UploadThing is a type-safe file upload service built for Next.js and React,
providing presigned uploads, file validation, and React components with minimal
configuration.

## Key Points

- Define all upload endpoints in one file router — type safety flows to the client
- Use middleware for auth — reject unauthorized uploads before they start
- Use `onUploadComplete` to save file references to your database
- Use the `useUploadThing` hook for custom upload UIs
- Set appropriate `maxFileSize` and `maxFileCount` per endpoint
- Return data from `onUploadComplete` — it's sent back to the client
- Not authenticating uploads — anyone can upload files
- Storing the full URL instead of the file key — URLs may change
- Not setting file size limits — allows abuse
- Building custom upload logic when UploadThing components handle it
- Not handling upload errors in the client — silent failures confuse users
- Using one generic upload endpoint for all file types — use separate routes

## Quick Example

```bash
npm install uploadthing @uploadthing/react
```

```env
UPLOADTHING_TOKEN=your-token
```
skilldb get storage-services-skills/UploadthingFull skill: 223 lines
Paste into your CLAUDE.md or agent config

UploadThing Integration

You are a file upload specialist who integrates UploadThing into projects. UploadThing is a type-safe file upload service built for Next.js and React, providing presigned uploads, file validation, and React components with minimal configuration.

Core Philosophy

Type-safe file routes

UploadThing defines upload endpoints as typed route handlers. Each route specifies allowed file types, max sizes, and middleware (auth checks). The TypeScript types flow from server to client — the upload component knows exactly what it can accept.

Zero client-side upload logic

UploadThing's React components handle the upload UI, progress tracking, and error states. You define the rules on the server, and the components enforce them automatically. No manual FormData or XHR code.

Setup

Install

npm install uploadthing @uploadthing/react

Environment variables

UPLOADTHING_TOKEN=your-token

Define file routes

// app/api/uploadthing/core.ts
import { createUploadthing, type FileRouter } from 'uploadthing/next';
import { auth } from '@/auth';

const f = createUploadthing();

export const ourFileRouter = {
  imageUploader: f({ image: { maxFileSize: '4MB', maxFileCount: 4 } })
    .middleware(async () => {
      const session = await auth();
      if (!session?.user) throw new Error('Unauthorized');
      return { userId: session.user.id };
    })
    .onUploadComplete(async ({ metadata, file }) => {
      console.log('Upload complete:', file.url);
      await db.insert(uploads).values({
        url: file.url,
        key: file.key,
        name: file.name,
        userId: metadata.userId,
      });
      return { url: file.url };
    }),

  documentUploader: f({ pdf: { maxFileSize: '16MB' }, text: { maxFileSize: '1MB' } })
    .middleware(async () => {
      const session = await auth();
      if (!session?.user) throw new Error('Unauthorized');
      return { userId: session.user.id };
    })
    .onUploadComplete(async ({ metadata, file }) => {
      return { url: file.url };
    }),

  avatarUploader: f({ image: { maxFileSize: '2MB', maxFileCount: 1 } })
    .middleware(async () => {
      const session = await auth();
      if (!session?.user) throw new Error('Unauthorized');
      return { userId: session.user.id };
    })
    .onUploadComplete(async ({ metadata, file }) => {
      await db.update(users)
        .set({ avatarUrl: file.url })
        .where(eq(users.id, metadata.userId));
      return { url: file.url };
    }),
} satisfies FileRouter;

export type OurFileRouter = typeof ourFileRouter;

Route handler

// app/api/uploadthing/route.ts
import { createRouteHandler } from 'uploadthing/next';
import { ourFileRouter } from './core';

export const { GET, POST } = createRouteHandler({ router: ourFileRouter });

Key Techniques

Upload components

'use client';
import { UploadButton, UploadDropzone } from '@uploadthing/react';
import type { OurFileRouter } from '@/app/api/uploadthing/core';

// Simple button
function ImageUpload() {
  return (
    <UploadButton<OurFileRouter, 'imageUploader'>
      endpoint="imageUploader"
      onClientUploadComplete={(res) => {
        console.log('Files:', res);
        const url = res[0]?.url;
      }}
      onUploadError={(error) => {
        alert(`Upload failed: ${error.message}`);
      }}
    />
  );
}

// Drag and drop
function DocumentUpload() {
  return (
    <UploadDropzone<OurFileRouter, 'documentUploader'>
      endpoint="documentUploader"
      onClientUploadComplete={(res) => {
        console.log('Uploaded:', res);
      }}
      onUploadError={(error) => {
        console.error(error);
      }}
    />
  );
}

useUploadThing hook (custom UI)

'use client';
import { useUploadThing } from '@uploadthing/react';

function CustomUploader() {
  const [files, setFiles] = useState<File[]>([]);

  const { startUpload, isUploading, permittedFileInfo } = useUploadThing('imageUploader', {
    onClientUploadComplete: (res) => {
      console.log('Done:', res);
      setFiles([]);
    },
    onUploadError: (err) => {
      console.error(err);
    },
    onUploadProgress: (progress) => {
      console.log(`${progress}%`);
    },
  });

  return (
    <div>
      <input
        type="file"
        accept="image/*"
        multiple
        onChange={(e) => setFiles(Array.from(e.target.files ?? []))}
      />
      <button
        onClick={() => startUpload(files)}
        disabled={isUploading || files.length === 0}
      >
        {isUploading ? 'Uploading...' : 'Upload'}
      </button>
    </div>
  );
}

Generate React component (with SSR utils)

// lib/uploadthing.ts
import { generateUploadButton, generateUploadDropzone } from '@uploadthing/react';
import type { OurFileRouter } from '@/app/api/uploadthing/core';

export const UploadButton = generateUploadButton<OurFileRouter>();
export const UploadDropzone = generateUploadDropzone<OurFileRouter>();

Delete files

import { UTApi } from 'uploadthing/server';

const utapi = new UTApi();

// Delete by key
await utapi.deleteFiles('file-key-here');

// Delete multiple
await utapi.deleteFiles(['key1', 'key2']);

Best Practices

  • Define all upload endpoints in one file router — type safety flows to the client
  • Use middleware for auth — reject unauthorized uploads before they start
  • Use onUploadComplete to save file references to your database
  • Use the useUploadThing hook for custom upload UIs
  • Set appropriate maxFileSize and maxFileCount per endpoint
  • Return data from onUploadComplete — it's sent back to the client

Anti-Patterns

  • Not authenticating uploads — anyone can upload files
  • Storing the full URL instead of the file key — URLs may change
  • Not setting file size limits — allows abuse
  • Building custom upload logic when UploadThing components handle it
  • Not handling upload errors in the client — silent failures confuse users
  • Using one generic upload endpoint for all file types — use separate routes

Install this skill directly: skilldb add storage-services-skills

Get CLI access →