Handles file uploads with UploadThing for TypeScript applications. Use when adding file uploads to Next.js or React apps with type-safe APIs, validation, and CDN delivery.
Adds type-safe file upload capabilities using UploadThing. Use when building Next.js/React apps that need file uploads with validation, pre-built components, and CDN delivery. Triggers when you need to implement upload endpoints, generate typed components, or handle server-side file operations.
/plugin marketplace add mgd34msu/goodvibes-plugin/plugin install goodvibes@goodvibes-marketThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Type-safe file uploads for Next.js and React. Define file routes, get pre-built components, automatic CDN delivery.
npm install uploadthing @uploadthing/react
UPLOADTHING_TOKEN# .env
UPLOADTHING_TOKEN=your_token_here
Define upload endpoints with validation and callbacks.
// app/api/uploadthing/core.ts
import { createUploadthing, type FileRouter } from "uploadthing/next";
const f = createUploadthing();
export const ourFileRouter = {
// Upload images up to 4MB
imageUploader: f({ image: { maxFileSize: "4MB", maxFileCount: 4 } })
.middleware(async ({ req }) => {
// Authenticate user
const user = await auth(req);
if (!user) throw new Error("Unauthorized");
// Return data accessible in onUploadComplete
return { userId: user.id };
})
.onUploadComplete(async ({ metadata, file }) => {
console.log("Upload complete:", file.url);
console.log("User:", metadata.userId);
// Return data to client
return { uploadedBy: metadata.userId };
}),
// Upload PDFs
pdfUploader: f({ pdf: { maxFileSize: "16MB" } })
.middleware(async () => ({ timestamp: Date.now() }))
.onUploadComplete(async ({ file }) => {
console.log("PDF uploaded:", file.url);
}),
// Any file type
fileUploader: f(["image", "video", "audio", "pdf", "text"])
.middleware(async () => ({}))
.onUploadComplete(async ({ file }) => {
console.log("File:", file.name, file.url);
}),
} satisfies FileRouter;
export type OurFileRouter = typeof ourFileRouter;
// app/api/uploadthing/route.ts
import { createRouteHandler } from "uploadthing/next";
import { ourFileRouter } from "./core";
export const { GET, POST } = createRouteHandler({
router: ourFileRouter,
});
// lib/uploadthing.ts
import {
generateUploadButton,
generateUploadDropzone,
generateUploader,
} from "@uploadthing/react";
import type { OurFileRouter } from "@/app/api/uploadthing/core";
export const UploadButton = generateUploadButton<OurFileRouter>();
export const UploadDropzone = generateUploadDropzone<OurFileRouter>();
export const Uploader = generateUploader<OurFileRouter>();
"use client";
import { UploadButton } from "@/lib/uploadthing";
export function ImageUpload() {
return (
<UploadButton
endpoint="imageUploader"
onClientUploadComplete={(res) => {
console.log("Files:", res);
// res = [{ url, name, size, key, serverData }]
}}
onUploadError={(error) => {
console.error("Error:", error.message);
}}
onUploadBegin={(name) => {
console.log("Uploading:", name);
}}
onUploadProgress={(progress) => {
console.log("Progress:", progress);
}}
/>
);
}
"use client";
import { UploadDropzone } from "@/lib/uploadthing";
export function FileDropzone() {
return (
<UploadDropzone
endpoint="fileUploader"
onClientUploadComplete={(res) => {
console.log("Uploaded:", res);
}}
onUploadError={(error) => {
alert(`Error: ${error.message}`);
}}
config={{
mode: "auto", // or "manual"
}}
/>
);
}
<UploadButton
endpoint="imageUploader"
appearance={{
button: "bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded",
container: "w-max flex-row gap-4",
allowedContent: "text-gray-500 text-sm",
}}
content={{
button: ({ ready, isUploading }) => {
if (isUploading) return "Uploading...";
if (ready) return "Upload Image";
return "Getting ready...";
},
allowedContent: "Images up to 4MB",
}}
onClientUploadComplete={(res) => console.log(res)}
/>
Full control over upload process.
"use client";
import { useUploadThing } from "@uploadthing/react";
import { useState, useCallback } from "react";
export function CustomUploader() {
const [files, setFiles] = useState<File[]>([]);
const { startUpload, isUploading, permittedFileInfo } = useUploadThing(
"imageUploader",
{
onClientUploadComplete: (res) => {
console.log("Uploaded:", res);
setFiles([]);
},
onUploadError: (error) => {
console.error("Error:", error);
},
onUploadProgress: (progress) => {
console.log("Progress:", progress);
},
}
);
const onDrop = useCallback((acceptedFiles: File[]) => {
setFiles(acceptedFiles);
}, []);
const handleUpload = () => {
if (files.length > 0) {
startUpload(files);
}
};
return (
<div>
<input
type="file"
multiple
onChange={(e) => setFiles(Array.from(e.target.files || []))}
accept={permittedFileInfo?.config?.image?.accept}
/>
<p>
Max size: {permittedFileInfo?.config?.image?.maxFileSize}
</p>
<button onClick={handleUpload} disabled={isUploading}>
{isUploading ? "Uploading..." : "Upload"}
</button>
</div>
);
}
Manage files from your server.
// lib/uploadthing-server.ts
import { UTApi } from "uploadthing/server";
export const utapi = new UTApi();
import { utapi } from "@/lib/uploadthing-server";
// Upload from URL
const response = await utapi.uploadFilesFromUrl([
"https://example.com/image.jpg",
{ url: "https://example.com/doc.pdf", name: "custom-name.pdf" }
]);
// Upload from File/Blob
const file = new File(["content"], "hello.txt", { type: "text/plain" });
const response = await utapi.uploadFiles([file]);
// Delete by key
await utapi.deleteFiles("fileKey123");
// Delete multiple
await utapi.deleteFiles(["key1", "key2"]);
// List all files
const files = await utapi.listFiles();
// Get file URLs
const urls = await utapi.getFileUrls(["key1", "key2"]);
// Rename file
await utapi.renameFiles({
fileKey: "oldName.jpg",
newName: "newName.jpg"
});
// Specific types
f({ image: { maxFileSize: "4MB" } })
f({ video: { maxFileSize: "256MB" } })
f({ audio: { maxFileSize: "16MB" } })
f({ pdf: { maxFileSize: "8MB" } })
f({ text: { maxFileSize: "1MB" } })
f({ blob: { maxFileSize: "8MB" } }) // Any binary
// Multiple types
f(["image", "video"])
// Custom MIME types
f({
"application/json": { maxFileSize: "1MB" },
"image/png": { maxFileSize: "4MB" }
})
f({
image: {
maxFileSize: "4MB",
maxFileCount: 10,
minFileCount: 1,
}
})
const f = createUploadthing();
f({ image: { maxFileSize: "4MB" } })
.input(z.object({
postId: z.string(),
caption: z.string().optional(),
}))
.middleware(async ({ input }) => {
// Access validated input
console.log("Post ID:", input.postId);
return { postId: input.postId };
})
.onUploadComplete(async ({ metadata, file }) => {
// Save to database with postId
await db.postImage.create({
data: {
postId: metadata.postId,
url: file.url,
},
});
});
<UploadButton
endpoint="imageUploader"
input={{ postId: "123", caption: "My photo" }}
onClientUploadComplete={(res) => console.log(res)}
/>
import { useForm } from "react-hook-form";
import { UploadButton } from "@/lib/uploadthing";
function PostForm() {
const form = useForm({
defaultValues: {
title: "",
imageUrl: "",
},
});
return (
<form onSubmit={form.handleSubmit(onSubmit)}>
<input {...form.register("title")} />
<UploadButton
endpoint="imageUploader"
onClientUploadComplete={(res) => {
form.setValue("imageUrl", res[0].url);
}}
/>
{form.watch("imageUrl") && (
<img src={form.watch("imageUrl")} alt="Preview" />
)}
<button type="submit">Submit</button>
</form>
);
}
// pages/api/uploadthing.ts
import { createRouteHandler } from "uploadthing/next-legacy";
import { ourFileRouter } from "../../server/uploadthing";
const handler = createRouteHandler({
router: ourFileRouter,
});
export default handler;
Improve initial load by including file route config in SSR.
// app/layout.tsx
import { NextSSRPlugin } from "@uploadthing/react/next-ssr-plugin";
import { extractRouterConfig } from "uploadthing/server";
import { ourFileRouter } from "@/app/api/uploadthing/core";
export default function RootLayout({ children }) {
return (
<html>
<body>
<NextSSRPlugin
routerConfig={extractRouterConfig(ourFileRouter)}
/>
{children}
</body>
</html>
);
}
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.