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 seamless with zero overhead.

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: 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

src/routes/api/upload/[...path]/+server.ts
import type { RequestHandler } from './$types';
import { uploadRouter } from '$lib/upload';

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

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

Basic Integration

Simple Upload Route

src/routes/api/upload/[...path]/+server.ts
import type { RequestHandler } from './$types';
import { uploadRouter } from '$lib/upload';

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

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

// Method 2: Separate handlers (if you need method-specific logic)
// export const { GET, POST } = uploadRouter.handlers;

With SvelteKit Hooks

src/hooks.server.ts
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

src/lib/upload.ts
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

Upload Component

src/lib/components/FileUpload.svelte
<script lang="ts">
  import { useUpload } from "pushduck/client";
  import type { AppUploadRouter } from "$lib/upload";
  
  const { UploadButton, UploadDropzone } = useUpload<AppUploadRouter>({
    endpoint: "/api/upload",
  });
  
  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>

<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 Pages

src/routes/+page.svelte
<script lang="ts">
  import FileUpload from "$lib/components/FileUpload.svelte";
</script>

<svelte:head>
  <title>File Upload Demo</title>
</svelte:head>

<main class="container mx-auto px-4 py-8">
  <h1 class="text-3xl font-bold mb-8">File Upload Demo</h1>
  <FileUpload />
</main>

File Management

Server-Side File Listing

src/routes/files/+page.server.ts
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,
    })),
  };
};
src/routes/files/+page.svelte
<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

svelte.config.js
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;
svelte.config.js
import adapter from '@sveltejs/adapter-netlify';

/** @type {import('@sveltejs/kit').Config} */
const config = {
  kit: {
    adapter: adapter({
      edge: false,
      split: false
    }),
  }
};

export default config;
svelte.config.js
import adapter from '@sveltejs/adapter-node';

/** @type {import('@sveltejs/kit').Config} */
const config = {
  kit: {
    adapter: adapter({
      out: 'build'
    }),
  }
};

export default config;
svelte.config.js
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

.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

# SvelteKit
PUBLIC_UPLOAD_ENDPOINT=http://localhost:5173/api/upload

Performance 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

src/lib/components/AdvancedUpload.svelte
<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

  1. Route not found: Ensure your route is src/routes/api/upload/[...path]/+server.ts
  2. Build errors: Check that pushduck is properly installed
  3. CORS issues: SvelteKit handles CORS automatically for same-origin requests

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 {};
});

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.