Framework Integrations

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

server/utils/upload.ts
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

server/api/upload/[...path].ts
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

server/api/upload/[...path].ts
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

server/middleware/cors.ts
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

server/utils/upload.ts
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

composables/useUpload.ts
import { useUpload } from "pushduck/client";
import type { AppUploadRouter } from "~/server/utils/upload";

export const { UploadButton, UploadDropzone } = useUpload<AppUploadRouter>({
  endpoint: "/api/upload",
});

Upload Component

components/FileUpload.vue
<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

pages/index.vue
<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

server/api/files.get.ts
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

pages/files.vue
<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

nuxt.config.ts
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,
  }
});
nuxt.config.ts
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,
  }
});
nuxt.config.ts
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,
  }
});
nuxt.config.ts
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

.env
# 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

components/AdvancedUpload.vue
<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

  1. Route not found: Ensure your route is server/api/upload/[...path].ts
  2. Build errors: Check that pushduck is properly installed
  3. CORS issues: Use server middleware for CORS configuration
  4. Runtime config: Make sure environment variables are properly configured

Debug Mode

Enable debug logging:

server/utils/upload.ts
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

nuxt.config.ts
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.