Skip to content

Instantly share code, notes, and snippets.

@danielkellyio
Created March 20, 2025 14:14
Show Gist options
  • Select an option

  • Save danielkellyio/3f2baee05fa908e7c13f2b83ea967e0b to your computer and use it in GitHub Desktop.

Select an option

Save danielkellyio/3f2baee05fa908e7c13f2b83ea967e0b to your computer and use it in GitHub Desktop.
Nuxt File Uploads with useStorage()
// server/api/upload/post.ts
// Endpoint to upload the files
import { H3Error } from "h3";
export default defineEventHandler(async (event) => {
// Parse multipart form data
const formData = await readMultipartFormData(event);
if (!formData || formData.length === 0) {
throw createError({
statusCode: 400,
statusMessage: "No files uploaded",
});
}
// Get storage instance
const storage = useStorage("uploads");
const uploadedFiles = [];
try {
// Process each file in the form data
for (const file of formData) {
// 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`,
});
}
// Validate file type
const allowedTypes = [
"image/jpeg",
"image/png",
"image/gif",
"application/pdf",
"application/msword",
"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(", ")}`,
});
}
if (!file || !file.filename) {
console.warn("Skipping invalid file entry");
continue;
}
// Store file using useStorage
const fileName = `${Date.now()}-${file.filename}`;
await storage.setItemRaw(`${fileName}`, file.data);
uploadedFiles.push({
filename: fileName,
url: `/uploads/${fileName}`,
});
}
return {
files: uploadedFiles,
};
} catch (error) {
if (error instanceof H3Error) {
throw error;
}
throw createError({
statusCode: 500,
statusMessage: "Error uploading files",
});
}
});
// -------------------------------------------------------------------------------------------------
// server/routes/uploads/[...path].ts
// Endpoint to serve the files
// (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);
});
@danielkellyio
Copy link
Author

Looking to add a file upload component to compliment this approach on the front-end? Checkout the Vue School course File Uploads in Vue.js

file-upload-course.mp4

@urbgimtam
Copy link

Very interesting.

But while storing in this fashion in /public/uploads, won't the contents be rewritten on each deploy / build in a production app?

Is there a nuxt way these files could be persistent on the server, between builds?

@urbgimtam
Copy link

Ok, nevermind. I've found a simple way. In nuxt.config.ts, doing something like the following seems to do the trick:

nitro: {
    storage: {
      uploads: {
        driver: "fs",
          base: process.env.NODE_ENV === "production"
            ? "./../.data/uploads"
            : "./.data/uploads",
      },
    },
  },

This way, its 'outside' the scope of the build step, preserving the files previously uploaded.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment