Skip to content

File Upload

15.4.1. FormData Construction

File uploads must use the FormData object and be sent with Content-Type set to multipart/form-data.

typescript
async function uploadFile(file: File, metadata: { category: string }) {
  const formData = new FormData()
  formData.append('file', file)
  formData.append('category', metadata.category)

  const response = await axios.post('/api/files', formData, {
    headers: { 'Content-Type': 'multipart/form-data' },
  })

  return response.data
}
  • The file and metadata must be included in a single FormData and sent as one request.
  • The Content-Type header must be explicitly set to multipart/form-data.
  • For multiple file uploads, append must be called repeatedly with the same key.

15.4.2. Progress Display

For large file uploads, the upload progress must be displayed using the Axios onUploadProgress callback.

vue
<script setup lang="ts">
import { ref } from 'vue'
import axios from 'axios'

const progress = ref(0)
const isUploading = ref(false)

async function upload(file: File) {
  isUploading.value = true
  progress.value = 0

  const formData = new FormData()
  formData.append('file', file)

  try {
    await axios.post('/api/files', formData, {
      headers: { 'Content-Type': 'multipart/form-data' },
      onUploadProgress(event) {
        if (event.total) {
          progress.value = Math.round((event.loaded / event.total) * 100)
        }
      },
    })
  } finally {
    isUploading.value = false
  }
}
</script>

<template>
  <div v-if="isUploading" class="progress-bar">
    <div class="progress-fill" :style="{ width: `${progress}%` }" />
    <span>{{ progress }}%</span>
  </div>
</template>
  • event.total may be undefined, so its existence must be checked.
  • The progress must be converted to an integer between 0 and 100 for display.

15.4.3. Drag and Drop

File drag and drop must be handled using the @dragover.prevent and @drop.prevent events.

vue
<script setup lang="ts">
import { ref } from 'vue'

const isDragOver = ref(false)
const selectedFile = ref<File | null>(null)

function handleDrop(event: DragEvent) {
  isDragOver.value = false
  const files = event.dataTransfer?.files
  if (files && files.length > 0) {
    selectedFile.value = files[0]
  }
}

function handleFileSelect(event: Event) {
  const target = event.target as HTMLInputElement
  if (target.files && target.files.length > 0) {
    selectedFile.value = target.files[0]
  }
}
</script>

<template>
  <div
    class="drop-zone"
    :class="{ 'drag-over': isDragOver }"
    @dragover.prevent="isDragOver = true"
    @dragleave="isDragOver = false"
    @drop.prevent="handleDrop"
  >
    <p>Drag a file here or click to select.</p>
    <input type="file" @change="handleFileSelect" />
  </div>
  <p v-if="selectedFile">Selected file: {{ selectedFile.name }}</p>
</template>
  • @dragover.prevent must be applied to prevent the browser's default behavior (opening the file).
  • The drag state (isDragOver) must be tracked to provide visual feedback.
  • An <input type="file"> must be placed alongside to support click-based selection as well.

15.4.4. Client-Side Validation

Before uploading a file, the extension, size, and MIME type must be validated on the client side.

typescript
// src/utils/fileValidation.ts
interface FileValidationResult {
  valid: boolean
  error: string | null
}

const ALLOWED_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'docx']
const MAX_FILE_SIZE = 50 * 1024 * 1024 // 50MB
const ALLOWED_MIME_TYPES = [
  'image/jpeg',
  'image/png',
  'image/gif',
  'application/pdf',
  'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
]

export function validateFile(file: File): FileValidationResult {
  const extension = file.name.split('.').pop()?.toLowerCase() ?? ''
  if (!ALLOWED_EXTENSIONS.includes(extension)) {
    return { valid: false, error: `File type not allowed. (${ALLOWED_EXTENSIONS.join(', ')})` }
  }

  if (file.size > MAX_FILE_SIZE) {
    return { valid: false, error: 'Maximum file size is 50MB.' }
  }

  if (!ALLOWED_MIME_TYPES.includes(file.type)) {
    return { valid: false, error: 'MIME type is not in the allowed list.' }
  }

  return { valid: true, error: null }
}
Validation ItemCriteriaNotes
ExtensionAllowlist-basedMust be synchronized with the backend allowlist
File sizeMaximum 50MBMay be adjusted per project requirements
MIME typeAllowlist-basedDual validation to prevent extension spoofing
  • The allowed extension list must be kept in sync with the backend standard.
  • Client-side validation is a preliminary check for user convenience and must not replace server-side validation.

TIENIPIA QUALIFIED STANDARD