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.
Let’s walk through building a complete file upload system in Nuxt using useStorage
:
First, create an API endpoint for handling file uploads.
// server/api/upload.ts
export default defineEventHandler(async (event) => {});
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];
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",
},
},
},
});
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`,
});
}
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(", ")}`,
});
}
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);
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;
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",
});
}
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}" />
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.
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!
© All rights reserved. Made with ❤️ by BitterBrains, Inc.