API Reference/Configuration

Client Configuration

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

Client Configuration

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

PropTypeDefault
onError?
(error: UploadError) => void
-
onProgress?
(progress: UploadProgress) => void
-
retries?
number
3
timeout?
number
30000
headers?
Record<string, string>
-
endpoint
string
-

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:

PropTypeDefault
abortSignal?
AbortSignal
-
metadata?
Record<string, any>
-
onError?
(error: UploadError) => void
-
onSuccess?
(results: UploadResult[]) => void
-
onProgress?
(progress: UploadProgress) => void
-

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

await uploadFiles(selectedFiles, {
  metadata: {
    category: 'contracts',
    department: 'legal',
    priority: 'high',
    tags: ['confidential', 'urgent']
  }
})
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:

PropTypeDefault
disabled?
boolean
false
onError?
(error: UploadError) => void
-
onSuccess?
(results: UploadResult[]) => void
-
endpoint
keyof AppRouter
-

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:

PropTypeDefault
chunkSize?
number
5242880
maxConcurrentUploads?
number
3
retryCondition?
(error: UploadError, attemptNumber: number) => boolean
-
retryDelays?
number[]
[1000, 2000, 4000]

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.