Home / Blog / Handling File Uploads in Nuxt with useStorage
Handling File Uploads in Nuxt with useStorage

Handling File Uploads in Nuxt with useStorage

Daniel Kelly
Daniel Kelly
Updated: April 10th 2025
This entry is part 2 of 2 in the series File Upload in Vue.js

Need robust file uploads in your Nuxt application? The useStorage function makes this essential feature simple and secure. Plus, it takes advantage of Unstorage’s unified storage API, meaning you can use it with almost any storage provider (such as local file storage, AWS S3, Cloudflare R2, etc) or switch providers at any time.

This guide demonstrates how to validate, store, and manage file uploads efficiently in Nuxt.js using built-in server utilities. Let’s build a production-ready upload system in just a few steps.

Step-by-Step Nuxt File Upload Implementation

Let’s walk through building a complete file upload system in Nuxt using useStorage:

Step 1: Create the Server Endpoint

First, create an API endpoint for handling file uploads.

// server/api/upload.ts
export default defineEventHandler(async (event) => {});

Step 2: Parse Multipart Form Data

Nuxt (well actually h3 under the hood) provides a handy readMultipartFormData function to extract files from incoming requests. We can get the form data, ensure there is at least one file uploaded, and then get the first file (you could also modify this to handle multiple files if needed with a loop).

const formData = await readMultipartFormData(event);
if (!formData || formData.length === 0) {
  throw createError({
    statusCode: 400,    statusMessage: "No files uploaded",  });
}
const file = formData[0];

Step 3: Set Up Storage Location

const storage = useStorage("uploads");

The useStorage composable is the heart of our implementation. By specifying "uploads" as the parameter, we’re telling Nuxt to use this “bucket” for storing uploaded files.

This bucket is mapped within the nuxt.config.ts file to the storage solution of your choice. For example, to use file storage:

// nuxt.config.tsexport default defineNuxtConfig({
  nitro: {
    storage: {
      uploads: {
        driver: "fs",        
        base: "./public/uploads",      
       },    
       },  
     },
});

Step 4: Validate File Size

Back in the API endpoint, you should add size validation to prevent users from overwhelming your server with large files.

// Validate file size (5MB limit)
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB in bytes
if (file.data.length > MAX_FILE_SIZE) {
  throw createError({
    statusCode: 400,    
    statusMessage: `File ${file.filename} exceeds maximum size of 5MB`,  
   });
 }

Step 5: Validate File Type

We can also add type validation to ensure only the allowed file types are accepted. This prevents upload of malicious files or files that are not of the desired type (and just take up space).

// Validate file type
const allowedTypes = [
  "image/jpeg",  "image/png",  "image/gif",  "application/pdf",  "text/plain",];

if (!file.type || !allowedTypes.includes(file.type)) {
    throw createError({
    statusCode: 400,    
    statusMessage: 
        `File type ${file.type || "unknown"} not allowed. 
        Allowed types: ${allowedTypes.join(", ")}`,  
    });
}

Step 6: Store Files with useStorage

We can save the file as binary data using the setItemRaw method. It’s also useful to generate a unique filename for the file before saving it so that we don’t overwrite any existing files. We’re using Date.now() to generate a unique filename but you could use a uuid.

// Store file using useStorage
const fileName = `${Date.now()}-${file.filename}`;
await storage.setItemRaw(`${fileName}`, file.data);

Step 7: Return the New File Name from the Endpoint

The uniquely generated filename will be it’s unique identifier, so we should return it from the endpoint to be used by the frontend for whatever purpose it has. (This could also be saved to a database if needed.)

return fileName;

Step 8: Wrap Everything in a Try Catch Block

To ensure a robust implementation, wrap everything in a try catch block. This will handle any errors that may occur during the upload process and return an appropriate HTTP response.

try {
  // Processing code here
} catch (error) {
  // check if the error is an instance of H3Error  
  // means we can pass on our validation errors to the frontend 
  // with the proper message and HTTP status code 
  if (error instanceof H3Error) {
    throw error;  
  }
  // this takes care of everything else  
  throw createError({
    statusCode: 500,    
    statusMessage: "Error uploading files",  
  });
}

Step 9: Setup a Server Route to Serve the File

Since we’re saving the file to the public directory, we could reach it directly from the frontend like this: /uploads/${fileName} but this isn’t really a production ready approach. Instead, we should setup a server route to serve the file so that it doesn’t matter where the file is stored.

// server/routes/uploads/[...path].ts
// (we use the routes folder just so the path doesn't include `/api` which feels cleaner)

export default defineEventHandler(async (event) => {
  const storage = useStorage("uploads");  
  const path = await getRouterParam(event, "path");  

  // you could also put in logic here  
  // to restrict access to the file based on  
  // the user, file type, or any other criteria  
  // but this implementation makes the file public  
  if (!path) {
        throw createError({
      statusCode: 400,      
      statusMessage: "Path is required",
      });  
    }
return await storage.getItemRaw(path);});

With this in place, we can now serve the file from the frontend with the url: /uploads/${fileName}. If this was an image, we could even display it directly in the frontend.

<img src="/uploads/${fileName}" />

The Complete Nuxt File Upload Solution Code

And that’s it! If you’d like to copy and paste the complete code for the file upload system, you can find it here.

In conclusion, Nuxt’s useStorage composable combined with its server utilities provides a powerful foundation for handling file uploads. By following the approach outlined in this article, you can implement secure, efficient file upload functionality in your Nuxt applications.

If you’d like to see a frontend implementation of the file upload system, then I recommend checking out our File Uploads in Vue.js Course. In it we use the same backend endpoint and storage location but add a frontend implementation so you’ll learn to create a robust file upload Vue component.

Descriptive alt text

Related Courses

Start learning Vue.js for free

Daniel Kelly
Daniel Kelly
Daniel is the lead instructor at Vue School and enjoys helping other developers reach their full potential. He has 10+ years of developer experience using technologies including Vue.js, Nuxt.js, and Laravel.

Comments

Latest Vue School Articles

The Model Context Protocol (MCP) for Web Developers

The Model Context Protocol (MCP) for Web Developers

Discover how the Model Context Protocol (MCP) can transform your web development workflow by connecting your IDE to Jira, Figma, databases, and more. Explore practical use cases
Daniel Kelly
Daniel Kelly
Build a File Upload Component in Vue.js with the Composition API

Build a File Upload Component in Vue.js with the Composition API

Learn to build a Vue.js file upload component using the Composition API. Master file selection, custom styling, multiple files support, and more in this hands-on tutorial.
Daniel Kelly
Daniel Kelly

Our goal is to be the number one source of Vue.js knowledge for all skill levels. We offer the knowledge of our industry leaders through awesome video courses for a ridiculously low price.

More than 200.000 users have already joined us. You are welcome too!

Follow us on Social

© All rights reserved. Made with ❤️ by BitterBrains, Inc.