file-upload
File upload patterns including multipart/form-data, presigned URLs for S3/R2, chunked uploads, progress tracking, drag-and-drop with react-dropzone, and client-side validation
Best use case
file-upload is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
File upload patterns including multipart/form-data, presigned URLs for S3/R2, chunked uploads, progress tracking, drag-and-drop with react-dropzone, and client-side validation
Teams using file-upload should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/file-upload/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How file-upload Compares
| Feature / Agent | file-upload | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
File upload patterns including multipart/form-data, presigned URLs for S3/R2, chunked uploads, progress tracking, drag-and-drop with react-dropzone, and client-side validation
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# File Upload Patterns Skill
## Purpose
Implement secure, efficient file uploads from client to server or directly to cloud storage. Covers validation, progress tracking, presigned URLs for large files, and drag-and-drop UX.
## Upload Strategy Decision
| Method | Max Size | Complexity | Best For |
|--------|----------|------------|----------|
| **multipart/form-data** | ~10MB | Low | Simple forms, small files |
| **Presigned URL (S3/R2)** | 5GB | Medium | Large files, serverless |
| **Chunked/Resumable** | Unlimited | High | Video, unreliable networks |
| **Base64 in JSON** | ~1MB | Low | Avatars, thumbnails only |
## Key Patterns
### 1. Server-Side: Presigned URL Generation
```typescript
// app/api/upload/route.ts
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { randomUUID } from 'crypto';
const s3 = new S3Client({
region: process.env.AWS_REGION!,
// For Cloudflare R2:
// endpoint: `https://${process.env.CF_ACCOUNT_ID}.r2.cloudflarestorage.com`,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
});
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'application/pdf'];
const MAX_SIZE = 10 * 1024 * 1024; // 10MB
export async function POST(req: Request) {
const { filename, contentType, size } = await req.json();
// Validate
if (!ALLOWED_TYPES.includes(contentType)) {
return Response.json({ error: 'File type not allowed' }, { status: 400 });
}
if (size > MAX_SIZE) {
return Response.json({ error: 'File too large (max 10MB)' }, { status: 400 });
}
const key = `uploads/${randomUUID()}/${filename}`;
const url = await getSignedUrl(
s3,
new PutObjectCommand({
Bucket: process.env.S3_BUCKET!,
Key: key,
ContentType: contentType,
ContentLength: size,
}),
{ expiresIn: 600 } // 10 minutes
);
return Response.json({ url, key });
}
```
### 2. Client Upload with Progress (Presigned URL)
```typescript
async function uploadWithProgress(
file: File,
onProgress: (percent: number) => void,
signal?: AbortSignal
): Promise<string> {
// Step 1: Get presigned URL from our API
const { url, key } = await fetch('/api/upload', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
filename: file.name,
contentType: file.type,
size: file.size,
}),
}).then((r) => r.json());
// Step 2: Upload directly to S3/R2 with progress
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('PUT', url);
xhr.setRequestHeader('Content-Type', file.type);
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) onProgress(Math.round((e.loaded / e.total) * 100));
};
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) resolve(key);
else reject(new Error(`Upload failed: ${xhr.status}`));
};
xhr.onerror = () => reject(new Error('Upload network error'));
if (signal) {
signal.addEventListener('abort', () => xhr.abort());
}
xhr.send(file);
});
}
```
### 3. React Dropzone Component
```tsx
'use client';
import { useCallback, useState } from 'react';
import { useDropzone, type FileRejection } from 'react-dropzone';
interface UploadZoneProps {
onUploadComplete: (key: string) => void;
maxSize?: number;
accept?: Record<string, string[]>;
}
export function UploadZone({
onUploadComplete,
maxSize = 10 * 1024 * 1024,
accept = { 'image/*': ['.jpg', '.jpeg', '.png', '.webp'] },
}: UploadZoneProps) {
const [progress, setProgress] = useState<number | null>(null);
const [error, setError] = useState<string | null>(null);
const onDrop = useCallback(async (accepted: File[], rejected: FileRejection[]) => {
setError(null);
if (rejected.length > 0) {
setError(rejected[0].errors[0].message);
return;
}
const file = accepted[0];
if (!file) return;
try {
setProgress(0);
const key = await uploadWithProgress(file, setProgress);
setProgress(100);
onUploadComplete(key);
} catch (err) {
setError(err instanceof Error ? err.message : 'Upload failed');
} finally {
setTimeout(() => setProgress(null), 2000);
}
}, [onUploadComplete]);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
maxSize,
accept,
maxFiles: 1,
});
return (
<div
{...getRootProps()}
className={`p-6 rounded-xl border-2 border-dashed cursor-pointer
transition-all duration-200
${isDragActive ? 'border-blue-500 bg-blue-50' : 'border-gray-300 hover:border-gray-400'}
focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-blue-500`}
>
<input {...getInputProps()} />
{progress !== null ? (
<div>
<div className="h-2 bg-gray-200 rounded-full overflow-hidden">
<div
className="h-full bg-blue-600 transition-all duration-200"
style={{ width: `${progress}%` }}
/>
</div>
<p className="mt-2 text-sm text-gray-600 text-center">{progress}%</p>
</div>
) : (
<p className="text-center text-gray-600">
{isDragActive ? 'Drop the file here' : 'Drag and drop a file, or click to select'}
</p>
)}
{error && <p className="mt-2 text-sm text-red-600 text-center">{error}</p>}
</div>
);
}
```
### 4. Next.js Route Handler (multipart/form-data)
```typescript
// app/api/upload-direct/route.ts -- for small files (< 10MB)
export async function POST(req: Request) {
const formData = await req.formData();
const file = formData.get('file') as File | null;
if (!file) {
return Response.json({ error: 'No file provided' }, { status: 400 });
}
// Validate MIME type by reading magic bytes (not just extension)
const buffer = Buffer.from(await file.arrayBuffer());
const mimeType = detectMimeType(buffer); // Use `file-type` package
if (!ALLOWED_TYPES.includes(mimeType)) {
return Response.json({ error: 'Invalid file type' }, { status: 400 });
}
// Upload to S3
await s3.send(new PutObjectCommand({
Bucket: process.env.S3_BUCKET!,
Key: `uploads/${randomUUID()}/${file.name}`,
Body: buffer,
ContentType: mimeType,
}));
return Response.json({ success: true });
}
```
### 5. Client-Side Validation
```typescript
interface ValidationRule {
maxSize: number;
allowedTypes: string[];
maxDimensions?: { width: number; height: number };
}
async function validateFile(file: File, rules: ValidationRule): Promise<string | null> {
if (file.size > rules.maxSize) {
return `File too large. Max ${(rules.maxSize / 1024 / 1024).toFixed(0)}MB.`;
}
if (!rules.allowedTypes.includes(file.type)) {
return `File type ${file.type} not allowed.`;
}
// Image dimension check
if (rules.maxDimensions && file.type.startsWith('image/')) {
const dimensions = await getImageDimensions(file);
if (dimensions.width > rules.maxDimensions.width || dimensions.height > rules.maxDimensions.height) {
return `Image too large. Max ${rules.maxDimensions.width}x${rules.maxDimensions.height}px.`;
}
}
return null; // Valid
}
function getImageDimensions(file: File): Promise<{ width: number; height: number }> {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve({ width: img.width, height: img.height });
img.onerror = reject;
img.src = URL.createObjectURL(file);
});
}
```
## Best Practices
1. **Use presigned URLs** for files > 5MB -- avoids proxying bytes through your server
2. **Validate on both client and server** -- client for UX, server for security
3. **Check magic bytes**, not just file extension -- extensions can be spoofed
4. **Generate unique keys** with UUID prefixes to prevent collisions and enumeration
5. **Set `Content-Disposition`** for downloadable files: `attachment; filename="original.pdf"`
6. **Limit upload size** at the infrastructure level too (Nginx `client_max_body_size`, Vercel 4.5MB body limit)
7. **Use abort controllers** to let users cancel in-progress uploads
## Common Pitfalls
| Pitfall | Impact | Fix |
|---------|--------|-----|
| No server-side validation | Malicious file uploads | Validate MIME type via magic bytes on server |
| Trusting `Content-Type` header | Attackers can spoof any type | Use `file-type` package to detect from bytes |
| Presigned URL without size limit | Attacker uploads huge files | Set `ContentLength` condition on presigned URL |
| No progress indicator | Users think upload is frozen | Use XMLHttpRequest `upload.onprogress` |
| Vercel 4.5MB body limit | Large uploads fail silently | Use presigned URLs for anything > 4MB |
| Sequential multi-file uploads | Slow UX | Upload files in parallel with `Promise.all` |Related Skills
UploadThing
> Type-safe file uploads for Next.js/React — no S3 config, built-in validation, instant CDN.
performance-profiler
Profile, benchmark, and identify performance bottlenecks in applications — CPU, memory, network, rendering, and database query performance
ultrathink
UltraThink Workflow OS — 4-layer skill mesh with persistent memory and privacy hooks for complex engineering tasks. Routes prompts through intent detection to activate the right domain skills automatically.
ultrathink_review
Multi-pass code review powered by UltraThink's quality gate — checks correctness, security (OWASP), performance, readability, and project conventions in a single structured pass.
ultrathink_memory
Persistent memory system for UltraThink — search, save, and recall project context, decisions, and patterns across sessions using Postgres-backed fuzzy search with synonym expansion.
ui-design
Comprehensive UI design system: 230+ font pairings, 48 themes, 65 design systems, 23 design languages, 30 UX laws, 14 color systems, Swiss grid, Gestalt principles, Pencil.dev workflow. Inherits ui-ux-pro-max (99 UX rules) + impeccable-frontend-design (anti-AI-slop). Triggers on any design, UI, layout, typography, color, theme, or styling task.
Zod
> TypeScript-first schema validation with static type inference.
webinar-registration-page
Build a webinar or live event registration page as a self-contained HTML file with countdown timer, speaker bio, agenda, and registration form. Triggers on: "build a webinar registration page", "create a webinar sign-up page", "event registration landing page", "live training registration page", "workshop sign-up page", "create a webinar page", "build an event page", "free webinar landing page", "live demo registration page", "online event page", "create a registration page for my webinar", "build a training event page".
webhooks
Webhook design patterns — delivery, retry with exponential backoff, HMAC signature verification, payload validation, idempotency keys
web-workers
Offload heavy computation from the main thread using Web Workers, SharedWorkers, and Comlink — structured messaging, transferable objects, and off-main-thread architecture patterns
web-vitals
Core Web Vitals monitoring (LCP, FID, CLS, INP, TTFB), measurement with web-vitals library, reporting to analytics, and optimization strategies for Next.js
web-components
Native Web Components, custom elements API, Shadow DOM, HTML templates, slots, lifecycle callbacks, and framework-agnostic design patterns