This guide covers the complete workflow for uploading files in your Nuxt application, from capturing user input to storing files on the server.
Overview
The file upload process consists of three main steps:
Capture files from input
Use the useFileStorage composable to handle file input events
Send files to backend
Use $fetch to send serialized files to your API endpoint
Store files on server
Use storeFileLocally to save files to the configured storage location
Frontend Setup
The useFileStorage composable automatically serializes files from file inputs, converting them to a format ready for transmission to the backend.
< template >
< input type = "file" @input = "handleFileInput" />
< button @click = "submit" > Upload </ button >
</ template >
< script setup >
const { handleFileInput , files } = useFileStorage ()
const submit = async () => {
const response = await $fetch ( '/api/files' , {
method: 'POST' ,
body: {
files: files . value
}
})
console . log ( 'Upload response:' , response )
}
</ script >
Multiple File Selection
Enable multiple file selection by adding the multiple attribute:
< template >
< input type = "file" multiple @input = "handleFileInput" />
</ template >
< script setup >
const { handleFileInput , files } = useFileStorage ()
</ script >
Composable Options
The useFileStorage composable accepts an options object:
When true, the files ref is cleared each time new files are selected. Set to false to accumulate files across multiple selections.
// Keep files from previous selections
const { handleFileInput , files } = useFileStorage ({ clearOldFiles: false })
Returned Values
A reactive ref containing the serialized files ready for upload. Each file includes:
name - Original filename
size - File size in bytes
type - MIME type
lastModified - Last modified timestamp
content - Base64-encoded data URL
handleFileInput
(event: Event) => Promise<void>
Event handler for file input changes. Returns a promise that resolves when all files are serialized.
Manually clear all files from the files ref.
Backend Implementation
Creating the API Route
Create an API route to receive and store uploaded files:
import { ServerFile } from "#file-storage/types"
export default defineEventHandler ( async ( event ) => {
const { files } = await readBody <{ files : ServerFile [] }>( event )
const fileNames : string [] = []
for ( const file of files ) {
// Store with auto-generated 8-character ID
const fileName = await storeFileLocally ( file , 8 , '/userFiles' )
fileNames . push ( fileName )
}
return fileNames
} )
Storage Options
The storeFileLocally function accepts three parameters:
The file object from the request body
Number : Generate a random ID with specified length (e.g., 8 creates an 8-character ID)
String : Use as the filename (extension will be auto-corrected if needed)
Subdirectory within the configured mount path. Use /userFiles, /images, etc.
Filename Examples
Auto-generated ID
Custom filename
Filename without extension
// Generates random ID like "aBcD1234.png"
const fileName = await storeFileLocally ( file , 8 , '/uploads' )
The file extension is automatically extracted from the MIME type or original filename. If you provide a custom filename with the wrong extension, it will be corrected and a warning logged.
Complete Working Example
Here’s a full example with file preview and upload feedback:
< template >
< div class = "upload-container" >
< label for = "file-input" class = "drop-zone" >
< span class = "drop-title" > Drop files here </ span >
< span > or </ span >
< input
id = "file-input"
type = "file"
multiple
@input = "handleFileInput"
/>
</ label >
< button @click = "submit" :disabled = "files.length === 0" >
Upload {{ files.length }} file(s)
</ button >
< p v-if = "uploadStatus" > {{ uploadStatus }} </ p >
<!-- File previews -->
< div class = "previews" >
< img
v-for = "file in files"
:key = "file.name"
:src = "file.content as string"
:alt = "file.name"
/>
</ div >
</ div >
</ template >
< script setup lang = "ts" >
const { handleFileInput , files } = useFileStorage ({ clearOldFiles: true })
const uploadStatus = ref ( '' )
const submit = async () => {
try {
uploadStatus . value = 'Uploading...'
const response = await $fetch ( '/api/files' , {
method: 'POST' ,
body: {
files: files . value
}
})
uploadStatus . value = `Uploaded ${ response . length } files successfully!`
console . log ( 'Uploaded files:' , response )
} catch ( error ) {
uploadStatus . value = 'Upload failed'
console . error ( 'Upload error:' , error )
}
}
</ script >
< style scoped >
.upload-container {
display : flex ;
flex-direction : column ;
gap : 1 rem ;
max-width : 500 px ;
margin : 0 auto ;
}
.drop-zone {
display : flex ;
flex-direction : column ;
align-items : center ;
gap : 10 px ;
padding : 2 rem ;
border : 2 px dashed #ccc ;
border-radius : 8 px ;
cursor : pointer ;
transition : background 0.2 s ;
}
.drop-zone:hover {
background : #f5f5f5 ;
}
.previews {
display : grid ;
grid-template-columns : repeat ( auto-fill , minmax ( 150 px , 1 fr ));
gap : 1 rem ;
}
.previews img {
width : 100 % ;
height : 150 px ;
object-fit : cover ;
border-radius : 4 px ;
}
</ style >
Advanced Usage
Waiting for File Serialization
The handleFileInput function returns a promise, allowing you to wait for serialization to complete:
const handleInput = async ( event : Event ) => {
await handleFileInput ( event )
console . log ( 'Files ready:' , files . value . length )
// Automatically upload after selection
await submit ()
}
Manual File Clearing
const { handleFileInput , files , clearFiles } = useFileStorage ()
const cancelUpload = () => {
clearFiles ()
console . log ( 'Upload cancelled' )
}
Parsing Data URLs Manually
If you need to work with the file data directly:
// parseDataUrl is auto-imported by Nuxt
export default defineEventHandler ( async ( event ) => {
const { files } = await readBody <{ files : ServerFile [] }>( event )
for ( const file of files ) {
// Extract binary data and extension
const { binaryString , ext } = parseDataUrl ( file . content )
// Process the binary data as needed
console . log ( `File extension: ${ ext } ` )
console . log ( `File size: ${ binaryString . length } bytes` )
}
} )
Security Considerations
Always validate file types, sizes, and content on the server side. The library includes path traversal protection, but you should implement additional validation based on your requirements.
File Type Validation
export default defineEventHandler ( async ( event ) => {
const { files } = await readBody <{ files : ServerFile [] }>( event )
const allowedTypes = [ 'image/png' , 'image/jpeg' , 'application/pdf' ]
for ( const file of files ) {
if ( ! allowedTypes . includes ( file . type )) {
throw createError ({
statusCode: 400 ,
message: `File type ${ file . type } not allowed`
})
}
await storeFileLocally ( file , 8 , '/uploads' )
}
} )
File Size Limits
const MAX_SIZE = 5 * 1024 * 1024 // 5MB
for ( const file of files ) {
if ( file . size > MAX_SIZE ) {
throw createError ({
statusCode: 413 ,
message: 'File too large'
})
}
}
Next Steps
Multiple Inputs Handle multiple file inputs on the same page
File Retrieval Serve and download uploaded files