Nuxt.js
Vue.js full-stack file uploads with Nuxt.js using Web Standards - no adapter needed!
🚧 Client-Side In Development: Nuxt.js server-side integration is fully functional with Web Standards APIs. However, Nuxt.js-specific client-side components and hooks are still in development. You can use the standard pushduck client APIs for now.
Using pushduck with Nuxt.js
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().maxFileSize("5MB"),
documentUpload: s3.file().maxFileSize("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()
.maxFileSize("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()
.maxFileSize("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.