SvelteKit
Fast, modern file uploads with SvelteKit using Web Standards - no adapter needed!
🚧 Client-Side In Development: SvelteKit server-side integration is fully functional with Web Standards APIs. However, SvelteKit-specific client-side components and hooks are still in development. You can use the standard pushduck client APIs for now.
Using pushduck with SvelteKit
SvelteKit is the official application framework for Svelte. It uses Web Standards APIs and provides excellent performance with minimal JavaScript. Since SvelteKit uses standard Request/Response objects, pushduck handlers work directly without any adapters!
Web Standards Native: SvelteKit server endpoints use Web Standard Request/Response objects, making pushduck integration straightforward with zero overhead.
Quick Setup
Install dependencies
npm install pushduckConfigure 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 type { RequestHandler } from './$types';
import { uploadRouter } from '$lib/upload';
// Direct usage - no adapter needed!
export const GET: RequestHandler = async ({ request }) => {
return uploadRouter.handler(request);
};
export const POST: RequestHandler = async ({ request }) => {
return uploadRouter.handler(request);
};Basic Integration
Simple Upload Route
import type { RequestHandler } from './$types';
import { uploadRouter } from '$lib/upload';
// Method 1: Combined handler (recommended)
export const GET: RequestHandler = async ({ request }) => {
return uploadRouter.handler(request);
};
export const POST: RequestHandler = async ({ request }) => {
return uploadRouter.handler(request);
};
// Method 2: Separate handlers (if you need method-specific logic)
// export const { GET, POST } = uploadRouter.handlers;With SvelteKit Hooks
import type { Handle } from '@sveltejs/kit';
import { sequence } from '@sveltejs/kit/hooks';
const corsHandler: Handle = async ({ event, resolve }) => {
if (event.url.pathname.startsWith('/api/upload')) {
if (event.request.method === 'OPTIONS') {
return new Response(null, {
status: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
},
});
}
}
const response = await resolve(event);
if (event.url.pathname.startsWith('/api/upload')) {
response.headers.set('Access-Control-Allow-Origin', '*');
}
return response;
};
export const handle = sequence(corsHandler);Advanced Configuration
Authentication with SvelteKit
import { createUploadConfig } from 'pushduck/server';
import { getUserFromSession } from '$lib/auth';
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 session 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;Client-Side Usage
SvelteKit uses the Svelte binding, pushduck/svelte. createUploadRoute returns a
readable store plus the imperative controls, so $upload auto-subscribes in
both Svelte 4 and Svelte 5.
pushduck/svelte requires Svelte 4+. It implements the store contract
structurally and does not import svelte/store, so it adds nothing to your
bundle beyond the upload logic itself.
Upload Component
<script lang="ts">
import { createUploadRoute } from "pushduck/svelte";
import type { AppUploadRouter } from "$lib/upload";
const upload = createUploadRoute<AppUploadRouter>("imageUpload", {
endpoint: "/api/upload",
onSuccess: (files) => console.log("Files uploaded:", files),
onError: (error) => console.error("Upload failed:", error),
});
function onSelect(event: Event) {
const input = event.target as HTMLInputElement;
upload.uploadFiles([...(input.files ?? [])]);
}
</script>
<div class="space-y-4">
<input
type="file"
multiple
accept="image/*"
disabled={$upload.isUploading}
on:change={onSelect}
/>
{#if $upload.isUploading}
<progress value={$upload.progress} max="100"></progress>
{/if}
{#each $upload.files as file (file.id)}
<div class="flex gap-2 items-center">
<span>{file.name}</span>
<span>{file.status} ({file.progress}%)</span>
{#if file.status === "uploading"}
<button on:click={() => upload.cancel(file.id)}>Cancel</button>
{/if}
{#if file.url}
<a href={file.url} target="_blank" rel="noreferrer">View</a>
{/if}
</div>
{/each}
{#each $upload.errors as error}
<p class="text-red-600">{error}</p>
{/each}
{#if $upload.files.length}
<button on:click={upload.reset}>Clear</button>
{/if}
</div>Using in Routes
<script lang="ts">
import FileUpload from "$lib/components/FileUpload.svelte";
</script>
<svelte:head>
<title>File Upload Demo</title>
</svelte:head>
<div class="container mx-auto px-4 py-8">
<h1 class="text-3xl font-bold mb-8">File Upload Demo</h1>
<FileUpload />
</div>Svelte 5 Runes
Stores remain first-class under runes, so the same code works unchanged. Derive from the store where you want a local reactive value:
<script lang="ts">
import { createUploadRoute } from "pushduck/svelte";
const upload = createUploadRoute("imageUpload", { endpoint: "/api/upload" });
let percent = $derived(Math.round($upload.progress));
let done = $derived($upload.files.filter((f) => f.status === "success"));
</script>
<p>{percent}% — {done.length} finished</p>Passing Client Metadata
<script lang="ts">
import { createUploadRoute } from "pushduck/svelte";
let albumId = "vacation-2026";
const upload = createUploadRoute("imageUpload", { endpoint: "/api/upload" });
function onSelect(event: Event) {
const input = event.target as HTMLInputElement;
upload.uploadFiles([...(input.files ?? [])], {
albumId,
visibility: "private",
});
}
</script>Client metadata is untrusted user input. Validate and sanitize it in your
server middleware, and never trust client-provided identity claims such as
userId or role — derive those from the authenticated session instead.
File Management
Server-Side File Listing
import type { PageServerLoad } from './$types';
import { db } from '$lib/database';
export const load: PageServerLoad = async ({ locals }) => {
const files = await db.file.findMany({
where: { userId: locals.user?.id },
orderBy: { createdAt: 'desc' },
});
return {
files: files.map(file => ({
id: file.id,
name: file.name,
url: file.url,
size: file.size,
uploadedAt: file.createdAt,
})),
};
};<script lang="ts">
import type { PageData } from './$types';
import FileUpload from '$lib/components/FileUpload.svelte';
export let data: PageData;
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>
<svelte:head>
<title>My Files</title>
</svelte:head>
<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>
{#if data.files.length === 0}
<p class="text-gray-500">No files uploaded yet.</p>
{:else}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{#each data.files as file}
<div 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>
{/each}
</div>
{/if}
</div>
</main>Deployment Options
import adapter from '@sveltejs/adapter-vercel';
import { vitePreprocess } from '@sveltejs/kit/vite';
/** @type {import('@sveltejs/kit').Config} */
const config = {
preprocess: vitePreprocess(),
kit: {
adapter: adapter({
runtime: 'nodejs18.x',
regions: ['iad1'],
}),
}
};
export default config;import adapter from '@sveltejs/adapter-netlify';
/** @type {import('@sveltejs/kit').Config} */
const config = {
kit: {
adapter: adapter({
edge: false,
split: false
}),
}
};
export default config;import adapter from '@sveltejs/adapter-node';
/** @type {import('@sveltejs/kit').Config} */
const config = {
kit: {
adapter: adapter({
out: 'build'
}),
}
};
export default config;import adapter from '@sveltejs/adapter-cloudflare';
/** @type {import('@sveltejs/kit').Config} */
const config = {
kit: {
adapter: adapter({
routes: {
include: ['/*'],
exclude: ['<build>']
}
}),
}
};
export default config;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
# SvelteKit
PUBLIC_UPLOAD_ENDPOINT=http://localhost:5173/api/uploadPerformance Benefits
Minimal JavaScript
Svelte compiles to vanilla JS with small bundle sizes
Web Standards
No adapter overhead - direct Request/Response usage
Fast Hydration
Selective hydration of interactive components
Edge Ready
Works on edge runtimes and CDNs
Real-Time Upload Progress
<script lang="ts">
import { onMount } from 'svelte';
import { writable } from 'svelte/store';
const uploadProgress = writable(0);
const isUploading = writable(false);
let fileInput: HTMLInputElement;
async function handleFileUpload(event: Event) {
const target = event.target as HTMLInputElement;
const files = target.files;
if (!files || files.length === 0) return;
isUploading.set(true);
uploadProgress.set(0);
try {
// Simulate upload progress
for (let i = 0; i <= 100; i += 10) {
uploadProgress.set(i);
await new Promise(resolve => setTimeout(resolve, 100));
}
alert('Upload completed!');
} catch (error) {
console.error('Upload failed:', error);
alert('Upload failed!');
} finally {
isUploading.set(false);
uploadProgress.set(0);
}
}
</script>
<div class="upload-container">
<input
bind:this={fileInput}
type="file"
multiple
on:change={handleFileUpload}
disabled={$isUploading}
class="file-input"
/>
{#if $isUploading}
<div class="progress-container">
<div class="progress-bar" style="width: {$uploadProgress}%"></div>
</div>
<p class="progress-text">{$uploadProgress}% uploaded</p>
{/if}
</div>
<style>
.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;
margin-top: 12px;
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
src/routes/api/upload/[...path]/+server.ts - Build errors: Check that pushduck is properly installed
- CORS issues: SvelteKit handles CORS automatically for same-origin requests
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 {};
});SvelteKit provides an excellent foundation for building fast, modern web applications with pushduck, combining the power of Svelte's reactive framework with Web Standards APIs.