Implement type-safe file uploads with UploadThing in Google Antigravity applications with validation and processing
# UploadThing Media Upload Patterns for Google Antigravity
File uploads are essential for modern applications but implementing them securely is challenging. This guide establishes patterns for integrating UploadThing with Google Antigravity projects, enabling Gemini 3 to generate type-safe, secure file upload implementations.
## Core Configuration
Set up UploadThing with proper file routers:
```typescript
// lib/uploadthing.ts
import { createUploadthing, type FileRouter } from "uploadthing/next";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
const f = createUploadthing();
const auth = async () => {
const session = await getServerSession(authOptions);
if (!session?.user) throw new Error("Unauthorized");
return { userId: session.user.id };
};
export const uploadRouter = {
// Profile image upload with size and type restrictions
profileImage: f({ image: { maxFileSize: "4MB", maxFileCount: 1 } })
.middleware(async () => {
const user = await auth();
return { userId: user.userId };
})
.onUploadComplete(async ({ metadata, file }) => {
console.log("Profile image uploaded for user:", metadata.userId);
console.log("File URL:", file.url);
// Update user profile in database
await db.user.update({
where: { id: metadata.userId },
data: { avatarUrl: file.url },
});
return { uploadedBy: metadata.userId, url: file.url };
}),
// Document upload with multiple file support
documents: f({
pdf: { maxFileSize: "16MB", maxFileCount: 5 },
"application/msword": { maxFileSize: "16MB", maxFileCount: 5 },
})
.middleware(async () => {
const user = await auth();
return { userId: user.userId };
})
.onUploadComplete(async ({ metadata, file }) => {
await db.document.create({
data: {
userId: metadata.userId,
fileName: file.name,
fileUrl: file.url,
fileSize: file.size,
},
});
return { url: file.url };
}),
// Media attachments for posts
postMedia: f({
image: { maxFileSize: "8MB", maxFileCount: 4 },
video: { maxFileSize: "64MB", maxFileCount: 1 },
})
.middleware(async ({ req }) => {
const user = await auth();
const postId = req.headers.get("x-post-id");
return { userId: user.userId, postId };
})
.onUploadComplete(async ({ metadata, file }) => {
if (metadata.postId) {
await db.postMedia.create({
data: {
postId: metadata.postId,
type: file.type.startsWith("video") ? "VIDEO" : "IMAGE",
url: file.url,
},
});
}
return { url: file.url };
}),
} satisfies FileRouter;
export type OurFileRouter = typeof uploadRouter;
```
## Upload Components
Create reusable upload components:
```typescript
// components/ImageUploader.tsx
"use client";
import { useCallback, useState } from "react";
import { useDropzone } from "@uploadthing/react";
import { generateClientDropzoneAccept } from "uploadthing/client";
import { useUploadThing } from "@/lib/uploadthing-client";
interface ImageUploaderProps {
onUploadComplete: (url: string) => void;
onUploadError?: (error: Error) => void;
currentImage?: string;
}
export function ImageUploader({
onUploadComplete,
onUploadError,
currentImage,
}: ImageUploaderProps) {
const [preview, setPreview] = useState<string | null>(currentImage || null);
const [isUploading, setIsUploading] = useState(false);
const { startUpload, permittedFileInfo } = useUploadThing("profileImage", {
onClientUploadComplete: (res) => {
setIsUploading(false);
if (res?.[0]) {
onUploadComplete(res[0].url);
}
},
onUploadError: (error) => {
setIsUploading(false);
onUploadError?.(error);
},
});
const onDrop = useCallback(
(acceptedFiles: File[]) => {
if (acceptedFiles[0]) {
setPreview(URL.createObjectURL(acceptedFiles[0]));
setIsUploading(true);
startUpload(acceptedFiles);
}
},
[startUpload]
);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
accept: generateClientDropzoneAccept(
permittedFileInfo?.config ? Object.keys(permittedFileInfo.config) : []
),
maxFiles: 1,
});
return (
<div
{...getRootProps()}
className={`border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-colors ${
isDragActive ? "border-blue-500 bg-blue-50" : "border-gray-300"
}`}
>
<input {...getInputProps()} />
{preview ? (
<img src={preview} alt="Preview" className="mx-auto h-32 w-32 rounded-full object-cover" />
) : (
<p>Drag & drop an image here, or click to select</p>
)}
{isUploading && <p className="mt-2 text-sm text-gray-500">Uploading...</p>}
</div>
);
}
```
## Best Practices
1. **Authentication**: Always verify user identity in middleware
2. **File validation**: Set appropriate size and type limits
3. **Error handling**: Provide clear feedback on upload failures
4. **Cleanup**: Delete old files when replacing uploads
5. **Progress indication**: Show upload progress for better UXThis File Upload prompt is ideal for developers working on:
By using this prompt, you can save hours of manual coding and ensure best practices are followed from the start. It's particularly valuable for teams looking to maintain consistency across their file upload implementations.
Yes! All prompts on Antigravity AI Directory are free to use for both personal and commercial projects. No attribution required, though it's always appreciated.
This prompt works excellently with Claude, ChatGPT, Cursor, GitHub Copilot, and other modern AI coding assistants. For best results, use models with large context windows.
You can modify the prompt by adding specific requirements, constraints, or preferences. For File Upload projects, consider mentioning your framework version, coding style, and any specific libraries you're using.