Framework Integrations

Qwik

Edge-optimized file uploads with Qwik using Web Standards - no adapter needed!

Qwik Integration

Qwik is a revolutionary web framework focused on resumability and edge optimization. It uses Web Standards APIs and provides instant loading with minimal JavaScript. Since Qwik uses standard Request/Response objects, pushduck handlers work directly without any adapters!

Web Standards Native: Qwik server endpoints use Web Standard Request/Response objects, making pushduck integration seamless with zero overhead and perfect for edge deployment.

Quick Setup

Install dependencies

npm install pushduck
yarn add pushduck
pnpm add pushduck
bun add pushduck

Configure upload router

src/lib/upload.ts
import { createUploadConfig } from 'pushduck/server';

const { s3, createS3Router } = createUploadConfig()
  .provider("cloudflareR2",{
    accessKeyId: import.meta.env.VITE_AWS_ACCESS_KEY_ID!,
    secretAccessKey: import.meta.env.VITE_AWS_SECRET_ACCESS_KEY!,
    region: 'auto',
    endpoint: import.meta.env.VITE_AWS_ENDPOINT_URL!,
    bucket: import.meta.env.VITE_S3_BUCKET_NAME!,
    accountId: import.meta.env.VITE_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

src/routes/api/upload/[...path]/index.ts
import type { RequestHandler } from '@builder.io/qwik-city';
import { uploadRouter } from '~/lib/upload';

// Direct usage - no adapter needed!
export const onGet: RequestHandler = async ({ request }) => {
  return uploadRouter.handlers(request);
};

export const onPost: RequestHandler = async ({ request }) => {
  return uploadRouter.handlers(request);
};

Basic Integration

Simple Upload Route

src/routes/api/upload/[...path]/index.ts
import type { RequestHandler } from '@builder.io/qwik-city';
import { uploadRouter } from '~/lib/upload';

// Method 1: Combined handler (recommended)
export const onRequest: RequestHandler = async ({ request }) => {
  return uploadRouter.handlers(request);
};

// Method 2: Separate handlers (if you need method-specific logic)
export const onGet: RequestHandler = async ({ request }) => {
  return uploadRouter.handlers.GET(request);
};

export const onPost: RequestHandler = async ({ request }) => {
  return uploadRouter.handlers.POST(request);
};

With CORS Support

src/routes/api/upload/[...path]/index.ts
import type { RequestHandler } from '@builder.io/qwik-city';
import { uploadRouter } from '~/lib/upload';

export const onRequest: RequestHandler = async ({ request, headers }) => {
  // Handle CORS preflight
  if (request.method === 'OPTIONS') {
    headers.set('Access-Control-Allow-Origin', '*');
    headers.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
    headers.set('Access-Control-Allow-Headers', 'Content-Type');
    return new Response(null, { status: 200 });
  }

  const response = await uploadRouter.handlers(request);
  
  // Add CORS headers to actual response
  headers.set('Access-Control-Allow-Origin', '*');
  
  return response;
};

Advanced Configuration

Authentication with Qwik

src/lib/upload.ts
import { createUploadConfig } from 'pushduck/server';

const { s3, createS3Router } = createUploadConfig()
  .provider("cloudflareR2",{
    accessKeyId: import.meta.env.VITE_AWS_ACCESS_KEY_ID!,
    secretAccessKey: import.meta.env.VITE_AWS_SECRET_ACCESS_KEY!,
    region: 'auto',
    endpoint: import.meta.env.VITE_AWS_ENDPOINT_URL!,
    bucket: import.meta.env.VITE_S3_BUCKET_NAME!,
    accountId: import.meta.env.VITE_R2_ACCOUNT_ID!,
  })
  .paths({
    prefix: 'uploads',
    generateKey: (file, metadata) => {
      return `${metadata.userId}/${Date.now()}/${file.name}`;
    }
  })
  .build();

export const uploadRouter = createS3Router({
  // Private uploads with cookie-based authentication
  privateUpload: s3
    .image()
    .max("5MB")
    .middleware(async ({ req }) => {
      const cookies = req.headers.get('Cookie');
      const sessionId = parseCookie(cookies)?.sessionId;
      
      if (!sessionId) {
        throw new Error('Authentication required');
      }
      
      const user = await getUserFromSession(sessionId);
      if (!user) {
        throw new Error('Invalid session');
      }
      
      return {
        userId: user.id,
        username: user.username,
      };
    }),

  // Public uploads (no auth)
  publicUpload: s3
    .image()
    .max("2MB")
    // No middleware = public access
});

export type AppUploadRouter = typeof uploadRouter;

// Helper functions
function parseCookie(cookieString: string | null) {
  if (!cookieString) return {};
  return Object.fromEntries(
    cookieString.split('; ').map(c => {
      const [key, ...v] = c.split('=');
      return [key, v.join('=')];
    })
  );
}

async function getUserFromSession(sessionId: string) {
  // Implement your session validation logic
  return { id: 'user-123', username: 'demo-user' };
}

Client-Side Usage

Upload Component

src/components/file-upload.tsx
import { component$, useSignal } from '@builder.io/qwik';
import { useUpload } from "pushduck/client";
import type { AppUploadRouter } from "~/lib/upload";

export const FileUpload = component$(() => {
  const uploadProgress = useSignal(0);
  const isUploading = useSignal(false);

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

  const handleUploadComplete = $((files: any[]) => {
    console.log("Files uploaded:", files);
    alert("Upload completed!");
  });

  const handleUploadError = $((error: Error) => {
    console.error("Upload error:", error);
    alert(`Upload failed: ${error.message}`);
  });

  return (
    <div class="space-y-6">
      <div>
        <h3 class="text-lg font-semibold mb-2">Image Upload</h3>
        <UploadButton
          endpoint="imageUpload"
          onClientUploadComplete$={handleUploadComplete}
          onUploadError$={handleUploadError}
        />
      </div>

      <div>
        <h3 class="text-lg font-semibold mb-2">Document Upload</h3>
        <UploadDropzone
          endpoint="documentUpload"
          onClientUploadComplete$={handleUploadComplete}
          onUploadError$={handleUploadError}
        />
      </div>
    </div>
  );
});

Using in Routes

src/routes/index.tsx
import { component$ } from '@builder.io/qwik';
import type { DocumentHead } from '@builder.io/qwik-city';
import { FileUpload } from '~/components/file-upload';

export default component$(() => {
  return (
    <main class="container mx-auto px-4 py-8">
      <h1 class="text-3xl font-bold mb-8">File Upload Demo</h1>
      <FileUpload />
    </main>
  );
});

export const head: DocumentHead = {
  title: 'File Upload Demo',
  meta: [
    {
      name: 'description',
      content: 'Qwik file upload demo with pushduck',
    },
  ],
};

File Management

Server-Side File Loader

src/routes/files/index.tsx
import { component$ } from '@builder.io/qwik';
import type { DocumentHead } from '@builder.io/qwik-city';
import { routeLoader$ } from '@builder.io/qwik-city';
import { FileUpload } from '~/components/file-upload';

export const useFiles = routeLoader$(async (requestEvent) => {
  const userId = 'current-user'; // Get from session/auth
  
  // Fetch files from database
  const files = await getFilesForUser(userId);
  
  return {
    files: files.map(file => ({
      id: file.id,
      name: file.name,
      url: file.url,
      size: file.size,
      uploadedAt: file.createdAt,
    })),
  };
});

export default component$(() => {
  const filesData = useFiles();
  
  const 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];
  };

  return (
    <main 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>
        
        {filesData.value.files.length === 0 ? (
          <p class="text-gray-500">No files uploaded yet.</p>
        ) : (
          <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
            {filesData.value.files.map((file) => (
              <div 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>
    </main>
  );
});

export const head: DocumentHead = {
  title: 'My Files',
};

async function getFilesForUser(userId: string) {
  // Implement your database query logic
  return [];
}

Deployment Options

vite.config.ts
import { defineConfig } from 'vite';
import { qwikVite } from '@builder.io/qwik/optimizer';
import { qwikCity } from '@builder.io/qwik-city/vite';
import { qwikCloudflarePages } from '@builder.io/qwik-city/adapters/cloudflare-pages/vite';

export default defineConfig(() => {
  return {
    plugins: [
      qwikCity({
        adapter: qwikCloudflarePages(),
      }),
      qwikVite(),
    ],
  };
});
vite.config.ts
import { defineConfig } from 'vite';
import { qwikVite } from '@builder.io/qwik/optimizer';
import { qwikCity } from '@builder.io/qwik-city/vite';
import { qwikVercel } from '@builder.io/qwik-city/adapters/vercel-edge/vite';

export default defineConfig(() => {
  return {
    plugins: [
      qwikCity({
        adapter: qwikVercel(),
      }),
      qwikVite(),
    ],
  };
});
vite.config.ts
import { defineConfig } from 'vite';
import { qwikVite } from '@builder.io/qwik/optimizer';
import { qwikCity } from '@builder.io/qwik-city/vite';
import { qwikNetlifyEdge } from '@builder.io/qwik-city/adapters/netlify-edge/vite';

export default defineConfig(() => {
  return {
    plugins: [
      qwikCity({
        adapter: qwikNetlifyEdge(),
      }),
      qwikVite(),
    ],
  };
});
vite.config.ts
import { defineConfig } from 'vite';
import { qwikVite } from '@builder.io/qwik/optimizer';
import { qwikCity } from '@builder.io/qwik-city/vite';
import { qwikDeno } from '@builder.io/qwik-city/adapters/deno/vite';

export default defineConfig(() => {
  return {
    plugins: [
      qwikCity({
        adapter: qwikDeno(),
      }),
      qwikVite(),
    ],
  };
});

Environment Variables

.env
# AWS Configuration
VITE_AWS_REGION=us-east-1
VITE_AWS_ACCESS_KEY_ID=your_access_key
VITE_AWS_SECRET_ACCESS_KEY=your_secret_key
VITE_AWS_S3_BUCKET=your-bucket-name

# Qwik
VITE_PUBLIC_UPLOAD_ENDPOINT=http://localhost:5173/api/upload

Performance Benefits

Resumability

Zero hydration - apps resume from server state

Web Standards

No adapter overhead - direct Request/Response usage

Edge Optimized

Perfect for edge deployment and CDNs

Instant Loading

O(1) JavaScript loading regardless of app size

Real-Time Upload Progress

src/components/advanced-upload.tsx
import { component$, useSignal, $ } from '@builder.io/qwik';

export const AdvancedUpload = component$(() => {
  const uploadProgress = useSignal(0);
  const isUploading = useSignal(false);

  const handleFileUpload = $(async (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;
    }
  });

  return (
    <div class="upload-container max-w-md mx-auto">
      <input
        type="file"
        multiple
        onChange$={handleFileUpload}
        disabled={isUploading.value}
        class="w-full p-3 border-2 border-dashed border-gray-300 rounded-lg cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
      />
      
      {isUploading.value && (
        <div class="mt-4">
          <div class="w-full h-2 bg-gray-200 rounded-full overflow-hidden">
            <div 
              class="h-full bg-green-500 transition-all duration-300 ease-out"
              style={{ width: `${uploadProgress.value}%` }}
            />
          </div>
          <p class="text-center mt-2 text-sm text-gray-600">
            {uploadProgress.value}% uploaded
          </p>
        </div>
      )}
    </div>
  );
});

Qwik City Form Integration

src/routes/upload-form/index.tsx
import { component$ } from '@builder.io/qwik';
import type { DocumentHead } from '@builder.io/qwik-city';
import { routeAction$, Form, zod$, z } from '@builder.io/qwik-city';
import { FileUpload } from '~/components/file-upload';

export const useUploadAction = routeAction$(async (data, requestEvent) => {
  // Handle form submission
  // Files are already uploaded via pushduck, just save metadata
  console.log('Form data:', data);
  
  // Redirect to files page
  throw requestEvent.redirect(302, '/files');
}, zod$({
  title: z.string().min(1),
  description: z.string().optional(),
}));

export default component$(() => {
  const uploadAction = useUploadAction();

  return (
    <main class="max-w-2xl mx-auto p-6">
      <h1 class="text-2xl font-bold mb-6">Upload Files</h1>
      
      <Form action={uploadAction} class="space-y-6">
        <div>
          <label for="title" class="block text-sm font-medium mb-2">
            Title
          </label>
          <input
            type="text"
            id="title"
            name="title"
            required
            class="w-full p-3 border border-gray-300 rounded-lg"
          />
        </div>
        
        <div>
          <label for="description" class="block text-sm font-medium mb-2">
            Description
          </label>
          <textarea
            id="description"
            name="description"
            rows={4}
            class="w-full p-3 border border-gray-300 rounded-lg"
          />
        </div>
        
        <div>
          <label class="block text-sm font-medium mb-2">
            Files
          </label>
          <FileUpload />
        </div>
        
        <button
          type="submit"
          disabled={uploadAction.isRunning}
          class="w-full bg-blue-500 text-white p-3 rounded-lg hover:bg-blue-600 disabled:opacity-50"
        >
          {uploadAction.isRunning ? "Uploading..." : "Upload Files"}
        </button>
      </Form>
    </main>
  );
});

export const head: DocumentHead = {
  title: 'Upload Form',
};

Troubleshooting

Common Issues

  1. Route not found: Ensure your route is src/routes/api/upload/[...path]/index.ts
  2. Build errors: Check that pushduck is properly installed and configured
  3. Environment variables: Use import.meta.env.VITE_ prefix for client-side variables
  4. Resumability: Remember to use $ suffix for event handlers and functions

Debug Mode

Enable debug logging:

src/lib/upload.ts
export const uploadRouter = createS3Router({
  // ... routes
}).middleware(async ({ req, file }) => {
  if (import.meta.env.DEV) {
    console.log("Upload request:", req.url);
    console.log("File:", file.name, file.size);
  }
  return {};
});

Qwik Configuration

vite.config.ts
import { defineConfig } from 'vite';
import { qwikVite } from '@builder.io/qwik/optimizer';
import { qwikCity } from '@builder.io/qwik-city/vite';

export default defineConfig(() => {
  return {
    plugins: [qwikCity(), qwikVite()],
    preview: {
      headers: {
        'Cache-Control': 'public, max-age=600',
      },
    },
    // Environment variables configuration
    define: {
      'import.meta.env.VITE_AWS_ACCESS_KEY_ID': JSON.stringify(process.env.VITE_AWS_ACCESS_KEY_ID),
    }
  };
});

Qwik provides a revolutionary approach to web development with pushduck, offering instant loading and resumability while maintaining full compatibility with Web Standards APIs for optimal edge performance.