typescriptintermediate

File Upload with Server Action

Handle file uploads using server actions with validation, size limits, and storage.

typescript
'use server';

import { writeFile, mkdir } from 'fs/promises';
import { join } from 'path';

const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'application/pdf'];

interface UploadResult {
  success: boolean;
  url?: string;
  error?: string;
}

export async function uploadFile(formData: FormData): Promise<UploadResult> {
  const file = formData.get('file') as File;

  if (!file || file.size === 0) {
    return { success: false, error: 'No file provided' };
  }

  if (file.size > MAX_FILE_SIZE) {
    return { success: false, error: 'File too large (max 5MB)' };
  }

  if (!ALLOWED_TYPES.includes(file.type)) {
    return { success: false, error: 'File type not allowed' };
  }

  try {
    const bytes = await file.arrayBuffer();
    const buffer = Buffer.from(bytes);

    // Generate unique filename
    const ext = file.name.split('.').pop();
    const filename = `${Date.now()}-${Math.random().toString(36).slice(2)}.${ext}`;

    const uploadDir = join(process.cwd(), 'public', 'uploads');
    await mkdir(uploadDir, { recursive: true });
    await writeFile(join(uploadDir, filename), buffer);

    return {
      success: true,
      url: `/uploads/${filename}`,
    };
  } catch {
    return { success: false, error: 'Upload failed' };
  }
}

// Client component:
// 'use client';
// export function UploadForm() {
//   const [result, setResult] = useState<UploadResult | null>(null);
//   return (
//     <form action={async (fd) => setResult(await uploadFile(fd))}>
//       <input type="file" name="file" accept="image/*,.pdf" />
//       <button type="submit">Upload</button>
//       {result?.error && <p className="text-red-500">{result.error}</p>}
//       {result?.url && <img src={result.url} alt="Uploaded" />}
//     </form>
//   );
// }

Use Cases

  • image uploads
  • document uploads
  • avatar changes

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.