Qwik
Edge-optimized file uploads with Qwik using Web Standards - no adapter needed!
🚧 Client-Side In Development: Qwik server-side integration is fully functional with Web Standards APIs. However, Qwik-specific client-side components and hooks are still in development. You can use the standard pushduck client APIs for now.
Using pushduck with Qwik
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 pushduckConfigure upload router
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().maxFileSize("5MB"),
documentUpload: s3.file().maxFileSize("10MB")
});
export type AppUploadRouter = typeof uploadRouter;Create API route
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
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
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
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()
.maxFileSize("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()
.maxFileSize("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
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
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
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
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(),
],
};
});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(),
],
};
});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(),
],
};
});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
# 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/uploadPerformance 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
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
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
- Route not found: Ensure your route is
src/routes/api/upload/[...path]/index.ts - Build errors: Check that pushduck is properly installed and configured
- Environment variables: Use
import.meta.env.VITE_prefix for client-side variables - Resumability: Remember to use
$suffix for event handlers and functions
Debug Mode
Enable debug logging:
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
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.