This guide covers how to handle file uploads using multipart/form-data with @qualisero/openapi-endpoint.
File uploads use the multipart/form-data content type, which allows sending binary files along with other form data. This library supports both:
- FormData objects - Directly upload browser
FormDatainstances - Binary strings - Upload binary data as string (for some APIs)
import { api } from './api/init'
async function uploadAvatar(userId: string, file: File) {
const formData = new FormData()
formData.append('avatar', file)
const uploadMutation = api.uploadUserAvatar.useMutation({ userId })
return uploadMutation.mutateAsync({
data: formData,
})
}
// Usage
const fileInput = document.querySelector('input[type="file"]')
const file = fileInput.files[0]
await uploadAvatar('123', file)import { api } from './api/init'
async function uploadDocument(userId: string, file: File, description: string) {
const formData = new FormData()
formData.append('document', file)
formData.append('description', description)
const uploadMutation = api.uploadUserDocument.useMutation({ userId })
return uploadMutation.mutateAsync({
data: formData,
})
}
await uploadDocument('123', file, 'Contract document')<script setup lang="ts">
import { ref } from 'vue'
import { api } from './api/init'
const fileInput = ref<HTMLInputElement | null>(null)
const isUploading = ref(false)
const uploadError = ref<string | null>(null)
const uploadAvatar = async (userId: string, file: File) => {
isUploading.value = true
uploadError.value = null
try {
const formData = new FormData()
formData.append('avatar', file)
const uploadMutation = api.uploadUserAvatar.useMutation({ userId })
await uploadMutation.mutateAsync({
data: formData,
})
alert('Upload successful!')
} catch (error) {
uploadError.value = error instanceof Error ? error.message : 'Upload failed'
} finally {
isUploading.value = false
}
}
const handleFileChange = (userId: string) => {
const file = fileInput.value?.files?.[0]
if (file) {
uploadAvatar(userId, file)
}
}
</script>
<template>
<div>
<input ref="fileInput" type="file" @change="handleFileChange('123')" accept="image/*" />
<p v-if="isUploading">Uploading...</p>
<p v-if="uploadError" class="error">{{ uploadError }}</p>
</div>
</template><script setup lang="ts">
import { ref } from 'vue'
import { api } from './api/init'
const file = ref<File | null>(null)
const uploadProgress = ref(0)
const isUploading = ref(false)
const uploadFile = async () => {
if (!file.value) return
isUploading.value = true
uploadProgress.value = 0
try {
const formData = new FormData()
formData.append('file', file.value)
const uploadMutation = api.uploadDocument.useMutation(
{},
{
axiosOptions: {
onUploadProgress: (progressEvent) => {
if (progressEvent.total) {
uploadProgress.value = Math.round((progressEvent.loaded * 100) / progressEvent.total)
}
},
},
},
)
await uploadMutation.mutateAsync({ data: formData })
} finally {
isUploading.value = false
}
}
</script>
<template>
<div>
<input type="file" @change="(e) => (file = e.target.files[0])" />
<button @click="uploadFile" :disabled="isUploading || !file">Upload</button>
<div v-if="isUploading" class="progress-bar">
<div class="progress-fill" :style="{ width: uploadProgress + '%' }"></div>
</div>
<p>{{ uploadProgress }}%</p>
</div>
</template>import { api } from './api/init'
async function uploadMultipleFiles(userId: string, files: File[]) {
const formData = new FormData()
files.forEach((file, index) => {
formData.append(`files[${index}]`, file)
// Or use same field name:
// formData.append('files', file)
})
const uploadMutation = api.uploadUserFiles.useMutation({ userId })
return uploadMutation.mutateAsync({
data: formData,
})
}
// Usage
const fileInput = document.querySelector('input[type="file"][multiple]')
const files = Array.from(fileInput.files)
await uploadMultipleFiles('123', files)Some APIs accept binary data as string instead of FormData:
import { api } from './api/init'
async function uploadBinaryData(userId: string, binaryString: string) {
const uploadMutation = api.uploadUserAvatar.useMutation({ userId })
return uploadMutation.mutateAsync({
data: {
file: binaryString, // Binary data as string
},
})
}
// Convert File to binary string
const file = fileInput.files[0]
const reader = new FileReader()
reader.onload = async () => {
const binaryString = reader.result as string
await uploadBinaryData('123', binaryString)
}
reader.readAsBinaryString(file)import { api } from './api/init'
const { data: userProfile } = api.getUserProfile.useQuery({ userId: '123' })
const uploadAvatar = async (userId: string, file: File) => {
const formData = new FormData()
formData.append('avatar', file)
const uploadMutation = api.uploadUserAvatar.useMutation(
{ userId },
{
// Automatically invalidate related queries after upload
invalidateOperations: ['getUserProfile'],
onSuccess: (data) => {
console.log('Avatar uploaded:', data)
// userProfile will automatically refetch
},
onError: (error) => {
console.error('Upload failed:', error)
},
},
)
return uploadMutation.mutateAsync({
data: formData,
})
}<script setup lang="ts">
import { ref } from 'vue'
import { api } from './api/init'
const file = ref<File | null>(null)
const error = ref<string | null>(null)
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/gif']
const MAX_SIZE = 5 * 1024 * 1024 // 5MB
const handleFileSelect = (e: Event) => {
const selectedFile = (e.target as HTMLInputElement).files?.[0]
if (!selectedFile) return
// Validate file type
if (!ALLOWED_TYPES.includes(selectedFile.type)) {
error.value = `Invalid file type. Allowed: ${ALLOWED_TYPES.join(', ')}`
return
}
// Validate file size
if (selectedFile.size > MAX_SIZE) {
error.value = 'File too large. Maximum size is 5MB'
return
}
file.value = selectedFile
error.value = null
}
const uploadFile = async () => {
if (!file.value) return
const formData = new FormData()
formData.append('file', file.value)
const uploadMutation = api.uploadDocument.useMutation(
{},
{
onSuccess: () => {
file.value = null
alert('Upload successful!')
},
onError: (err) => {
error.value = err instanceof Error ? err.message : 'Upload failed'
},
},
)
await uploadMutation.mutateAsync({ data: formData })
}
</script>
<template>
<div>
<input type="file" accept="image/jpeg,image/png,image/gif" @change="handleFileSelect" />
<button @click="uploadFile" :disabled="!file">Upload</button>
<p v-if="error" class="error">{{ error }}</p>
</div>
</template><script setup lang="ts">
import { ref } from 'vue'
import { api } from './api/init'
const file = ref<File | null>(null)
const previewUrl = ref<string | null>(null)
const handleFileSelect = (e: Event) => {
const selectedFile = (e.target as HTMLInputElement).files?.[0]
if (!selectedFile) return
file.value = selectedFile
// Create preview URL
previewUrl.value = URL.createObjectURL(selectedFile)
}
const uploadFile = async () => {
if (!file.value) return
const formData = new FormData()
formData.append('avatar', file.value)
const uploadMutation = api.uploadUserAvatar.useMutation(
{ userId: '123' },
{
onSuccess: () => {
// Cleanup preview
if (previewUrl.value) {
URL.revokeObjectURL(previewUrl.value)
previewUrl.value = null
}
file.value = null
},
},
)
await uploadMutation.mutateAsync({ data: formData })
}
</script>
<template>
<div>
<input type="file" @change="handleFileSelect" accept="image/*" />
<div v-if="previewUrl" class="preview">
<img :src="previewUrl" alt="Preview" />
<button @click="uploadFile">Upload</button>
</div>
</div>
</template>-
Validate files on client - Check file type and size before uploading to save bandwidth
-
Show upload progress - Use axios's
onUploadProgressto show feedback to users -
Invalidate cache on success - Automatically refresh related queries after upload
-
Handle errors gracefully - Show clear error messages when uploads fail
-
Clean up resources - Use
URL.revokeObjectURL()for preview URLs -
Use FormData for uploads - It's the standard way to upload files in browsers
-
Test with real files - File uploads can have issues that don't appear with small test data
- Axios Configuration - Learn about advanced Axios configuration for uploads and more
- Cache Management - Learn about advanced cache control strategies