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

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().maxFileSize("5MB"),
  documentUpload: s3.file().maxFileSize("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()
    .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

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.