Pushduck
Pushduck// S3 uploads for any framework

Client Configuration

Configure your upload client for the best developer experience with enhanced type inference

Client Setup Options

The upload client provides multiple APIs to suit different needs: property-based access for enhanced type safety, and hook-based access for familiar React patterns.

This guide focuses on the enhanced client API with property-based access. This provides the best developer experience with full TypeScript inference and eliminates string literals.

Client Setup Structure

Organize your client configuration for maximum reusability:

upload.ts
upload-client.ts

Basic Client Configuration

Import your router types

Start by importing the router type from your server configuration:

lib/upload-client.ts
import { createUploadClient } from 'pushduck/client'
import type { AppRouter } from './upload'

export const upload = createUploadClient<AppRouter>({
  endpoint: '/api/upload'
})

Use property-based access

Access your upload endpoints as properties with full type safety:

components/upload-form.tsx
import { upload } from '@/lib/upload-client'

export function ImageUploadForm() {
  const { uploadFiles, files, isUploading, error } = upload.imageUpload
  //           ^ Full TypeScript inference from your server router

  const handleUpload = async (selectedFiles: File[]) => {
    await uploadFiles(selectedFiles)
  }

  return (
    <div>
      <input type="file" onChange={(e) => handleUpload(Array.from(e.target.files || []))} />
      {files.map(file => (
        <div key={file.id}>
          <span>{file.name}</span>
          <span>{file.status}</span> {/* 'pending' | 'uploading' | 'success' | 'error' */}
          <progress value={file.progress} max={100} />
          {file.url && <a href={file.url}>View</a>}
        </div>
      ))}
    </div>
  )
}

Handle upload results

Process upload results with full type safety:

const { uploadFiles, files, reset } = upload.documentUpload

const handleDocumentUpload = async (files: File[]) => {
  try {
    const results = await uploadFiles(files)
    // results is fully typed based on your router configuration

    console.log('Upload successful:', results.map(r => r.url))

    // Reset the upload state
    reset()
  } catch (error) {
    console.error('Upload failed:', error)
  }
}

Client Configuration Options

Prop

Type

Advanced Client Configuration

lib/upload-client.ts
import { createUploadClient } from "pushduck/client";
import type { AppRouter } from "./upload";

export const upload = createUploadClient<AppRouter>({
  endpoint: "/api/upload",

  // Custom headers (e.g., authentication)
  headers: {
    Authorization: `Bearer ${getAuthToken()}`,
    "X-Client-Version": "1.0.0",
  },

  // Upload timeout (30 seconds)
  timeout: 30000,

  // Retry failed uploads
  retries: 3,

  // Global progress tracking
  onProgress: (progress) => {
    console.log(`Upload progress: ${progress.percentage}%`);
  },

  // Global error handling
  onError: (error) => {
    console.error("Upload error:", error);
    // Send to error tracking service
    trackError(error);
  },
});

Upload Method Options

Each upload method accepts configuration options:

Prop

Type

Upload Method Examples

const { uploadFiles } = upload.imageUpload

// Simple upload
const results = await uploadFiles(selectedFiles)
console.log('Uploaded files:', results)
const { uploadFiles } = upload.imageUpload

await uploadFiles(selectedFiles, {
  onProgress: (progress) => {
    console.log(`Upload ${progress.percentage}% complete`)
    updateProgressBar(progress.percentage)
  },

  onSuccess: (results) => {
    console.log('Upload successful!', results)
    showSuccessNotification()
  },

  onError: (error) => {
    console.error('Upload failed:', error)
    showErrorNotification(error.message)
  }
})
const { uploadFiles } = upload.documentUpload

// Pass metadata as second parameter (not in options)
await uploadFiles(selectedFiles, {
  category: 'contracts',
  department: 'legal',
  priority: 'high',
  tags: ['confidential', 'urgent'],
  projectId: currentProject.id,
  uploadedBy: currentUser.email
})
const { uploadFiles } = upload.videoUpload
const abortController = new AbortController()

// Start upload
const uploadPromise = uploadFiles(selectedFiles, {
  abortSignal: abortController.signal,
  onProgress: (progress) => {
    if (progress.percentage > 50 && shouldCancel) {
      abortController.abort()
    }
  }
})

// Cancel upload after 10 seconds
setTimeout(() => abortController.abort(), 10000)

try {
  await uploadPromise
} catch (error) {
  if (error.name === 'AbortError') {
    console.log('Upload was cancelled')
  }
}

Hook-Based API (Alternative)

For teams that prefer React hooks, the hook-based API provides a familiar pattern:

Prop

Type

Hook Usage Examples

import { useUpload } from 'pushduck/client'
import type { AppRouter } from '@/lib/upload'

export function ImageUploadComponent() {
  const { uploadFiles, files, isUploading, error, reset } = useUpload<AppRouter>('imageUpload', {
    onSuccess: (results) => {
      console.log('Upload completed:', results)
    },
    onError: (error) => {
      console.error('Upload failed:', error)
    }
  })

  return (
    <div>
      <input
        type="file"
        multiple
        onChange={(e) => uploadFiles(Array.from(e.target.files || []))}
        disabled={isUploading}
      />

      {files.map(file => (
        <div key={file.id}>
          <span>{file.name}</span>
          <progress value={file.progress} max={100} />
          {file.status === 'error' && <span>Failed: {file.error}</span>}
          {file.status === 'success' && <a href={file.url}>View</a>}
        </div>
      ))}

      <button onClick={reset} disabled={isUploading}>
        Reset
      </button>
    </div>
  )
}
export function MultiUploadComponent() {
  const images = useUpload<AppRouter>('imageUpload')
  const documents = useUpload<AppRouter>('documentUpload')

  return (
    <div>
      <div>
        <h3>Images</h3>
        <input
          type="file"
          accept="image/*"
          multiple
          onChange={(e) => images.uploadFiles(Array.from(e.target.files || []))}
        />
        {/* Render images.files */}
      </div>

      <div>
        <h3>Documents</h3>
        <input
          type="file"
          accept=".pdf,.doc,.docx"
          onChange={(e) => documents.uploadFiles(Array.from(e.target.files || []))}
        />
        {/* Render documents.files */}
      </div>
    </div>
  )
}
import { useUpload } from 'pushduck/client'
import { useCallback } from 'react'
import type { AppRouter } from '@/lib/upload'

export function useImageUpload() {
  const upload = useUpload<AppRouter>('imageUpload', {
    onSuccess: (results) => {
      // Show success toast
      toast.success(`Uploaded ${results.length} images`)
    },
    onError: (error) => {
      // Show error toast
      toast.error(`Upload failed: ${error.message}`)
    }
  })

  const uploadImages = useCallback(async (files: File[]) => {
    // Validate files before upload
    const validFiles = files.filter(file => {
      if (file.size > 5 * 1024 * 1024) { // 5MB
        toast.error(`${file.name} is too large`)
        return false
      }
      return true
    })

    if (validFiles.length > 0) {
      await upload.uploadFiles(validFiles)
    }
  }, [upload.uploadFiles])

  return {
    ...upload,
    uploadImages
  }
}

Property-Based Client Access

The property-based client provides enhanced type inference and eliminates string literals:

Type Safety Benefits

Compile-Time Validation

Catch errors before runtime

const { uploadFiles } = upload.imageUpload
//                            ^ TypeScript knows this exists

const { uploadFiles: docUpload } = upload.nonExistentEndpoint
//                                        ^ TypeScript error!

IntelliSense Support

Full autocomplete for all endpoints

upload. // IntelliSense shows: imageUpload, documentUpload, videoUpload
//   ^ All your router endpoints are available with autocomplete

Refactoring Safety

Rename endpoints safely across your codebase

// If you rename 'imageUpload' to 'images' in your router,
// TypeScript will show errors everywhere it's used,
// making refactoring safe and easy

Enhanced Type Inference

The property-based client provides complete type inference from your server router:

// Server router definition
export const router = createUploadRouter({
  profilePictures: uploadSchema({
    image: { maxSize: "2MB", maxCount: 1 },
  }).middleware(async ({ req }) => {
    const userId = await getUserId(req);
    return { userId, category: "profile" };
  }),

  // ... other endpoints
});

// Client usage with full type inference
const { uploadFiles, files, isUploading } = upload.profilePictures;
//      ^ uploadFiles knows it accepts File[]
//                   ^ files has type UploadFile[]
//                           ^ isUploading is boolean

// Upload files with inferred return type
const results = await uploadFiles(selectedFiles);
//    ^ results is UploadResult[] with your specific metadata shape

Framework-Specific Configuration

// app/lib/upload-client.ts
import { createUploadClient } from 'pushduck/client'
import type { AppRouter } from './upload'

export const upload = createUploadClient<AppRouter>({
  endpoint: '/api/upload',
  headers: {
    // Next.js specific headers
    'x-requested-with': 'pushduck'
  }
})

// app/components/upload-form.tsx
'use client'

import { upload } from '@/lib/upload-client'

export function UploadForm() {
  const { uploadFiles, files, isUploading } = upload.imageUpload

  // Component implementation...
}
// src/lib/upload-client.ts
import { createUploadClient } from 'pushduck/client'
import type { AppRouter } from './upload'

export const upload = createUploadClient<AppRouter>({
  endpoint: process.env.REACT_APP_UPLOAD_ENDPOINT || '/api/upload'
})

// src/components/UploadForm.tsx
import React from 'react'
import { upload } from '../lib/upload-client'

export function UploadForm() {
  const { uploadFiles, files, isUploading } = upload.imageUpload

  // Component implementation...
}
// lib/upload-client.ts
import { createUploadClient } from '@pushduck/vue'
import type { AppRouter } from './upload'

export const upload = createUploadClient<AppRouter>({
  endpoint: '/api/upload'
})

// components/UploadForm.vue
<script setup lang="ts">
import { upload } from '@/lib/upload-client'

const { uploadFiles, files, isUploading } = upload.imageUpload
// Vue 3 Composition API with full type safety
</script>
// lib/upload-client.ts
import { uploadStore } from '@pushduck/svelte'
import type { AppRouter } from './upload'

export const upload = uploadStore<AppRouter>('/api/upload')

// components/UploadForm.svelte
<script lang="ts">
import { upload } from '../lib/upload-client'

// Reactive stores
$: ({ files, isUploading } = $upload.imageUpload)
</script>

Error Handling Configuration

Configure comprehensive error handling for robust applications:

Prop

Type

Advanced Error Handling

export const upload = createUploadClient<AppRouter>({
  endpoint: "/api/upload",

  // Custom retry configuration
  retries: 3,
  retryDelays: [1000, 2000, 4000], // 1s, 2s, 4s

  // Custom retry logic
  retryCondition: (error, attemptNumber) => {
    // Don't retry client errors (4xx)
    if (error.status >= 400 && error.status < 500) {
      return false;
    }

    // Retry server errors up to 3 times
    return attemptNumber < 3;
  },

  // Concurrent upload limits
  maxConcurrentUploads: 2,

  // Large file chunking
  chunkSize: 10 * 1024 * 1024, // 10MB chunks

  // Global error handler
  onError: (error) => {
    // Log to error tracking service
    if (error.status >= 500) {
      logError("Server error during upload", error);
    }

    // Show user-friendly message
    if (error.code === "FILE_TOO_LARGE") {
      showToast("File is too large. Please choose a smaller file.");
    } else if (error.code === "NETWORK_ERROR") {
      showToast("Network error. Please check your connection.");
    } else {
      showToast("Upload failed. Please try again.");
    }
  },
});

Performance Optimization

Configure the client for optimal performance:

Upload Performance

export const upload = createUploadClient<AppRouter>({
  endpoint: "/api/upload",

  // Optimize for performance
  maxConcurrentUploads: 3, // Balance between speed and resource usage
  chunkSize: 5 * 1024 * 1024, // 5MB chunks for large files
  timeout: 60000, // 60 second timeout for large files

  // Compression for images
  compressImages: {
    enabled: true,
    quality: 0.8, // 80% quality
    maxWidth: 1920, // Resize large images
    maxHeight: 1080,
  },

  // Connection pooling
  keepAlive: true,
  maxSockets: 5,

  // Progress throttling to avoid UI updates spam
  progressThrottle: 100, // Update progress every 100ms
});

Real-World Configuration Examples

E-commerce Application

export const ecommerceUpload = createUploadClient<EcommerceRouter>({
  endpoint: "/api/upload",

  headers: {
    Authorization: `Bearer ${getAuthToken()}`,
    "X-Store-ID": getStoreId(),
  },

  onProgress: (progress) => {
    // Update global upload progress indicator
    updateGlobalProgress(progress);
  },

  onError: (error) => {
    // Track upload failures for analytics
    analytics.track("upload_failed", {
      error_code: error.code,
      file_type: error.metadata?.fileType,
      store_id: getStoreId(),
    });
  },

  // E-commerce specific settings
  retries: 2, // Quick retries for better UX
  maxConcurrentUploads: 5, // Allow multiple product images
  compressImages: {
    enabled: true,
    quality: 0.9, // High quality for product images
  },
});

// Usage in product form
export function ProductImageUpload() {
  const { uploadFiles, files, isUploading } = ecommerceUpload.productImages;

  const handleImageUpload = async (files: File[]) => {
    await uploadFiles(files, {
      metadata: {
        productId: getCurrentProductId(),
        category: "product-images",
      },
      onSuccess: (results) => {
        updateProductImages(results.map((r) => r.url));
      },
    });
  };

  return (
    // Upload component implementation
    <div>{/* Upload UI */}</div>
  );
}

Content Management System

export const cmsUpload = createUploadClient<CMSRouter>({
  endpoint: "/api/upload",

  headers: {
    Authorization: `Bearer ${getAuthToken()}`,
    "X-Workspace": getCurrentWorkspace(),
  },

  // CMS-specific configuration
  timeout: 120000, // 2 minutes for large documents
  retries: 3,
  maxConcurrentUploads: 2, // Conservative for large files

  onError: (error) => {
    // Show contextual error messages
    if (error.code === "QUOTA_EXCEEDED") {
      showUpgradeModal();
    } else if (error.code === "UNAUTHORIZED") {
      redirectToLogin();
    }
  },
});

// Usage in content editor
export function MediaLibrary() {
  const images = cmsUpload.images;
  const documents = cmsUpload.documents;
  const videos = cmsUpload.videos;

  return (
    <div>
      <MediaUploadTabs>
        <Tab name="Images">
          <UploadZone {...images} accept="image/*" />
        </Tab>
        <Tab name="Documents">
          <UploadZone {...documents} accept=".pdf,.doc,.docx" />
        </Tab>
        <Tab name="Videos">
          <UploadZone {...videos} accept="video/*" />
        </Tab>
      </MediaUploadTabs>
    </div>
  );
}

Ready to upload? Check out our complete examples to see these configurations in action, or explore our provider setup guides to configure your storage backend.