# Frontend Vite File Upload

> Vite admin FormData file uploads: build multipart in api.ts, file preview in forms, image URL display. Use when implementing image uploads in admin CRUD forms.

- Skill: `xmuhameed/frontend-vite-file-upload` (Agent Skill)
- Install (CLI): `npx skillmds@latest add xmuhameed/frontend-vite-file-upload`
- Raw SKILL.md: https://api.skillmd.com/api/skills/xmuhameed/frontend-vite-file-upload/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: xmuhameed (https://skillmd.com/u/xmuhameed)
- Updated: 2026-09-21
- Page: https://skillmd.com/skills/xmuhameed/frontend-vite-file-upload

---


# Admin File Upload

**Different from backend** — frontend builds FormData, backend handles two-phase save.

## API Layer

```typescript
export const createCategory = async (data: CreateCategoryPayload) => {
  const formData = new FormData();
  Object.entries(data).forEach(([key, value]) => {
    if (value != null && !['imageUrl', 'coverUrl'].includes(key)) {
      formData.append(key, value.toString());
    }
  });
  if (data.imageUrl) formData.append('imageUrl', data.imageUrl);
  if (data.coverUrl) formData.append('coverUrl', data.coverUrl);

  return (await api.post('/category/create-category', formData, {
    headers: { 'Content-Type': 'multipart/form-data' },
  })).data;
};
```

**Exclude file keys** from Object.entries loop — append File objects separately.

## Form UI

```tsx
<input type="file" accept="image/*" hidden ref={fileRef} onChange={handleFileChange} />
<Button type="button" onClick={() => fileRef.current?.click()}>Upload</Button>
{preview && <img src={preview} alt="Preview" />}
```

```typescript
const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
  const file = e.target.files?.[0];
  if (!file?.type.startsWith('image/')) { toast.error('Image only'); return; }
  if (file.size > 5 * 1024 * 1024) { toast.error('Max 5MB'); return; }
  setImageFile(file);
  setPreview(URL.createObjectURL(file));
};
```

## Submit

```typescript
await createMutation.mutateAsync({ ...formData, imageUrl: imageFile ?? undefined });
```

## Edit Form — Current vs New

Show existing: `` `${API_URL}${entity.imageUrl}` ``
Show new preview from FileReader/ObjectURL alongside.

## JSON Fallback (products)

If no files, some endpoints accept JSON body instead of FormData.

## Axios Note

No default `Content-Type` on axios instance — allows multipart boundary auto-set.

