Nuxt.js
Vue.js full-stack file uploads with Nuxt.js using Web Standards - no adapter needed!
Nuxt.js Integration
Nuxt.js is the intuitive Vue.js framework for building full-stack web applications. It uses Web Standards APIs and provides excellent performance with server-side rendering. Since Nuxt.js uses standard Request
/Response
objects, pushduck handlers work directly without any adapters!
Web Standards Native: Nuxt.js server routes use Web Standard Request
/Response
objects, making pushduck integration seamless with zero overhead.
Quick Setup
Install dependencies
npm install pushduck
yarn add pushduck
pnpm add pushduck
bun add pushduck
Configure upload router
import { createUploadConfig } from 'pushduck/server';
const { s3, createS3Router } = createUploadConfig()
.provider("cloudflareR2",{
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
region: 'auto',
endpoint: process.env.AWS_ENDPOINT_URL!,
bucket: process.env.S3_BUCKET_NAME!,
accountId: process.env.R2_ACCOUNT_ID!,
})
.build();
export const uploadRouter = createS3Router({
imageUpload: s3.image().max("5MB"),
documentUpload: s3.file().max("10MB")
});
export type AppUploadRouter = typeof uploadRouter;
Create API route
import { uploadRouter } from '~/server/utils/upload';
// Direct usage - no adapter needed!
export default defineEventHandler(async (event) => {
return uploadRouter.handlers(event.node.req);
});
Basic Integration
Simple Upload Route
import { uploadRouter } from '~/server/utils/upload';
// Method 1: Combined handler (recommended)
export default defineEventHandler(async (event) => {
return uploadRouter.handlers(event.node.req);
});
// Method 2: Method-specific handlers
export default defineEventHandler({
onRequest: [
// Add middleware here if needed
],
handler: async (event) => {
if (event.node.req.method === 'GET') {
return uploadRouter.handlers.GET(event.node.req);
}
if (event.node.req.method === 'POST') {
return uploadRouter.handlers.POST(event.node.req);
}
}
});
With Server Middleware
export default defineEventHandler(async (event) => {
if (event.node.req.url?.startsWith('/api/upload')) {
// Handle CORS for upload routes
setHeader(event, 'Access-Control-Allow-Origin', '*');
setHeader(event, 'Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
setHeader(event, 'Access-Control-Allow-Headers', 'Content-Type');
if (event.node.req.method === 'OPTIONS') {
return '';
}
}
});
Advanced Configuration
Authentication with Nuxt
import { createUploadConfig } from 'pushduck/server';
import jwt from 'jsonwebtoken';
const { s3, createS3Router } = createUploadConfig()
.provider("cloudflareR2",{
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
region: 'auto',
endpoint: process.env.AWS_ENDPOINT_URL!,
bucket: process.env.S3_BUCKET_NAME!,
accountId: process.env.R2_ACCOUNT_ID!,
})
.paths({
prefix: 'uploads',
generateKey: (file, metadata) => {
return `${metadata.userId}/${Date.now()}/${file.name}`;
}
})
.build();
export const uploadRouter = createS3Router({
// Private uploads with JWT authentication
privateUpload: s3
.image()
.max("5MB")
.middleware(async ({ req }) => {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
throw new Error('Authorization required');
}
const token = authHeader.substring(7);
try {
const payload = jwt.verify(token, process.env.JWT_SECRET!) as any;
return {
userId: payload.sub,
userRole: payload.role
};
} catch (error) {
throw new Error('Invalid token');
}
}),
// Public uploads (no auth)
publicUpload: s3
.image()
.max("2MB")
// No middleware = public access
});
export type AppUploadRouter = typeof uploadRouter;
Client-Side Usage
Upload Composable
import { useUpload } from "pushduck/client";
import type { AppUploadRouter } from "~/server/utils/upload";
export const { UploadButton, UploadDropzone } = useUpload<AppUploadRouter>({
endpoint: "/api/upload",
});
Upload Component
<template>
<div class="space-y-6">
<div>
<h3 class="text-lg font-semibold mb-2">Image Upload</h3>
<UploadButton
endpoint="imageUpload"
@client-upload-complete="handleUploadComplete"
@upload-error="handleUploadError"
/>
</div>
<div>
<h3 class="text-lg font-semibold mb-2">Document Upload</h3>
<UploadDropzone
endpoint="documentUpload"
@client-upload-complete="handleUploadComplete"
@upload-error="handleUploadError"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { UploadButton, UploadDropzone } from "~/composables/useUpload";
function handleUploadComplete(files: any[]) {
console.log("Files uploaded:", files);
alert("Upload completed!");
}
function handleUploadError(error: Error) {
console.error("Upload error:", error);
alert(`Upload failed: ${error.message}`);
}
</script>
Using in Pages
<template>
<div class="container mx-auto px-4 py-8">
<h1 class="text-3xl font-bold mb-8">File Upload Demo</h1>
<FileUpload />
</div>
</template>
<script setup lang="ts">
import FileUpload from "~/components/FileUpload.vue";
useHead({
title: 'File Upload Demo'
});
</script>
File Management
Server-Side File API
export default defineEventHandler(async (event) => {
const query = getQuery(event);
const userId = query.userId as string;
if (!userId) {
throw createError({
statusCode: 400,
statusMessage: 'User ID required'
});
}
// Fetch files from database
const files = await $fetch('/api/database/files', {
query: { userId }
});
return {
files: files.map((file: any) => ({
id: file.id,
name: file.name,
url: file.url,
size: file.size,
uploadedAt: file.createdAt,
})),
};
});
File Management Page
<template>
<div class="container mx-auto px-4 py-8">
<h1 class="text-3xl font-bold mb-8">My Files</h1>
<div class="mb-8">
<FileUpload />
</div>
<div>
<h2 class="text-2xl font-semibold mb-4">Uploaded Files</h2>
<div v-if="pending" class="text-center py-8">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto"></div>
<p class="mt-2 text-gray-500">Loading files...</p>
</div>
<div v-else-if="!data?.files?.length" class="text-gray-500">
No files uploaded yet.
</div>
<div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div
v-for="file in data.files"
:key="file.id"
class="border rounded-lg p-4 hover:shadow-md transition-shadow"
>
<h3 class="font-medium truncate" :title="file.name">
{{ file.name }}
</h3>
<p class="text-sm text-gray-500">
{{ formatFileSize(file.size) }}
</p>
<p class="text-sm text-gray-500">
{{ new Date(file.uploadedAt).toLocaleDateString() }}
</p>
<a
:href="file.url"
target="_blank"
rel="noopener noreferrer"
class="text-blue-500 hover:underline text-sm mt-2 inline-block"
>
View File
</a>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import FileUpload from "~/components/FileUpload.vue";
useHead({
title: 'My Files'
});
const { data, pending } = await $fetch('/api/files', {
query: { userId: 'current-user' }
});
function formatFileSize(bytes: number): string {
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
if (bytes === 0) return '0 Bytes';
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
}
</script>
Deployment Options
export default defineNuxtConfig({
nitro: {
preset: 'vercel-edge',
// or 'vercel' for Node.js runtime
},
runtimeConfig: {
awsAccessKeyId: process.env.AWS_ACCESS_KEY_ID,
awsSecretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
awsRegion: process.env.AWS_REGION,
s3BucketName: process.env.S3_BUCKET_NAME,
}
});
export default defineNuxtConfig({
nitro: {
preset: 'netlify-edge',
// or 'netlify' for Node.js runtime
},
runtimeConfig: {
awsAccessKeyId: process.env.AWS_ACCESS_KEY_ID,
awsSecretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
awsRegion: process.env.AWS_REGION,
s3BucketName: process.env.S3_BUCKET_NAME,
}
});
export default defineNuxtConfig({
nitro: {
preset: 'node-server',
},
runtimeConfig: {
awsAccessKeyId: process.env.AWS_ACCESS_KEY_ID,
awsSecretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
awsRegion: process.env.AWS_REGION,
s3BucketName: process.env.S3_BUCKET_NAME,
}
});
export default defineNuxtConfig({
nitro: {
preset: 'cloudflare-pages',
},
runtimeConfig: {
awsAccessKeyId: process.env.AWS_ACCESS_KEY_ID,
awsSecretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
awsRegion: process.env.AWS_REGION,
s3BucketName: process.env.S3_BUCKET_NAME,
}
});
Environment Variables
# AWS Configuration
AWS_REGION=us-east-1
AWS_ACCESS_KEY_ID=your_access_key
AWS_SECRET_ACCESS_KEY=your_secret_key
AWS_S3_BUCKET=your-bucket-name
# JWT Secret (for authentication)
JWT_SECRET=your-jwt-secret
# Nuxt
NUXT_PUBLIC_UPLOAD_ENDPOINT=http://localhost:3000/api/upload
Performance Benefits
Universal Rendering
SSR, SPA, or static generation for optimal performance
Web Standards
No adapter overhead - direct Request/Response usage
Auto Imports
Automatic imports for components and composables
Edge Ready
Works on edge runtimes with Nitro
Real-Time Upload Progress
<template>
<div class="upload-container">
<input
ref="fileInput"
type="file"
multiple
@change="handleFileUpload"
:disabled="isUploading"
class="file-input"
/>
<div v-if="isUploading" class="mt-4">
<div class="progress-container">
<div
class="progress-bar"
:style="{ width: `${uploadProgress}%` }"
></div>
</div>
<p class="progress-text">{{ uploadProgress }}% uploaded</p>
</div>
</div>
</template>
<script setup lang="ts">
const fileInput = ref<HTMLInputElement>();
const uploadProgress = ref(0);
const isUploading = ref(false);
async function handleFileUpload(event: Event) {
const target = event.target as HTMLInputElement;
const files = target.files;
if (!files || files.length === 0) return;
isUploading.value = true;
uploadProgress.value = 0;
try {
// Simulate upload progress
for (let i = 0; i <= 100; i += 10) {
uploadProgress.value = i;
await new Promise(resolve => setTimeout(resolve, 100));
}
alert('Upload completed!');
} catch (error) {
console.error('Upload failed:', error);
alert('Upload failed!');
} finally {
isUploading.value = false;
uploadProgress.value = 0;
}
}
</script>
<style scoped>
.upload-container {
max-width: 400px;
margin: 0 auto;
}
.file-input {
width: 100%;
padding: 12px;
border: 2px dashed #ccc;
border-radius: 8px;
cursor: pointer;
}
.file-input:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.progress-container {
width: 100%;
height: 8px;
background-color: #f0f0f0;
border-radius: 4px;
overflow: hidden;
}
.progress-bar {
height: 100%;
background-color: #4CAF50;
transition: width 0.3s ease;
}
.progress-text {
text-align: center;
margin-top: 8px;
font-size: 14px;
color: #666;
}
</style>
Troubleshooting
Common Issues
- Route not found: Ensure your route is
server/api/upload/[...path].ts
- Build errors: Check that pushduck is properly installed
- CORS issues: Use server middleware for CORS configuration
- Runtime config: Make sure environment variables are properly configured
Debug Mode
Enable debug logging:
export const uploadRouter = createS3Router({
// ... routes
}).middleware(async ({ req, file }) => {
if (process.dev) {
console.log("Upload request:", req.url);
console.log("File:", file.name, file.size);
}
return {};
});
Nitro Configuration
export default defineNuxtConfig({
nitro: {
experimental: {
wasm: true
},
// Enable debugging in development
devProxy: {
'/api/upload': {
target: 'http://localhost:3000/api/upload',
changeOrigin: true
}
}
}
});
Nuxt.js provides an excellent foundation for building full-stack Vue.js applications with pushduck, combining the power of Vue's reactive framework with Web Standards APIs and Nitro's universal deployment capabilities.