Uploadthing
Build with UploadThing for file uploads in Next.js and React. Use this skill when
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 linesUploadThing 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
onUploadCompleteto save file references to your database - Use the
useUploadThinghook for custom upload UIs - Set appropriate
maxFileSizeandmaxFileCountper 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
Related Skills
AWS S3
Build with AWS S3 for object storage. Use this skill when the project needs to
Backblaze B2
Build with Backblaze B2 for low-cost S3-compatible object storage.
Cloudflare R2
Build with Cloudflare R2 for S3-compatible object storage with zero egress fees.
Cloudinary
Build with Cloudinary for image and video management. Use this skill when the
Imagekit
Build with ImageKit for real-time image optimization and delivery. Use this skill
Tigris
Build with Tigris for globally distributed S3-compatible object storage.