Skip to content

Instantly share code, notes, and snippets.

@danielkellyio
Created March 20, 2025 14:14
Show Gist options
  • Save danielkellyio/3f2baee05fa908e7c13f2b83ea967e0b to your computer and use it in GitHub Desktop.
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

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