API Reference/Utilities

createUploadClient

Create a type-safe upload client with property-based access and optional per-route configuration

createUploadClient

Create a type-safe upload client with property-based access and optional per-route configuration. This is the recommended approach for most projects.

Enhanced in v2.0: Now supports per-route callbacks, progress tracking, and error handling while maintaining superior type safety.

Why Use This Approach?

  • 🏆 Superior Type Safety - Route names validated at compile time

  • 🎯 Property-Based Access - No string literals, full IntelliSense

  • Per-Route Configuration - Callbacks, endpoints, and options per route

  • 🔄 Centralized Setup - Single configuration for all routes

  • 🛡️ Refactoring Safety - Rename routes safely across codebase

    This utility function provides property-based access to your upload routes. You can also use the useUploadRoute<AppRouter>() hook if you prefer traditional React patterns.

Basic Setup

Create the upload client

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

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

Use in components

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

export function UploadForm() {
  const { uploadFiles, files, isUploading } = upload.imageUpload()
  
  return (
    <input 
      type="file" 
      onChange={(e) => uploadFiles(Array.from(e.target.files || []))}
      disabled={isUploading}
    />
  )
}

Configuration Options

PropTypeDefault
fetcher?
(input: RequestInfo, init?: RequestInit) => Promise<Response>
-
defaultOptions?
RouteUploadOptions
-
endpoint
string
-

Per-Route Configuration

Each route method now accepts optional configuration:

PropTypeDefault
autoUpload?
boolean
true
disabled?
boolean
false
endpoint?
string
-
onProgress?
(progress: number) => void
-
onError?
(error: Error) => void
-
onSuccess?
(results: UploadResult[]) => void
-

Examples

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

    export function BasicUpload() {
      // Simple usage - no configuration needed
      const { uploadFiles, files, isUploading, reset } = upload.imageUpload()

      return (
        <div>
          <input 
            type="file"
            accept="image/*"
            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 === 'success' && <span>✅</span>}
            </div>
          ))}
          
          <button onClick={reset}>Reset</button>
        </div>
      )
    }
import { upload } from '@/lib/upload-client'
import { toast } from 'sonner'

export function CallbackUpload() {
  const [progress, setProgress] = useState(0)
  
  const { uploadFiles, files, isUploading } = upload.imageUpload({
    onSuccess: (results) => {
      toast.success(`✅ Uploaded ${results.length} images!`)
      console.log('Upload results:', results)
    },
    onError: (error) => {
      toast.error(`❌ Upload failed: ${error.message}`)
      console.error('Upload error:', error)
    },
    onProgress: (progress) => {
      setProgress(progress)
      console.log(`📊 Progress: ${progress}%`)
    }
  })

  return (
    <div>
      <input 
        type="file"
        multiple
        accept="image/*"
        onChange={(e) => uploadFiles(Array.from(e.target.files || []))}
      />
      
      {progress > 0 && (
        <div>
          <progress value={progress} max={100} />
          <span>{progress}% complete</span>
        </div>
      )}
    </div>
  )
}
export function MultiUploadForm() {
  // Different configuration for each upload type
  const images = upload.imageUpload({
    onSuccess: (results) => updateImageGallery(results)
  })
  
  const documents = upload.documentUpload({
    onSuccess: (results) => updateDocumentLibrary(results),
    onError: (error) => logSecurityError(error)
  })
  
  const videos = upload.videoUpload({
    onProgress: (progress) => setVideoProgress(progress),
    onSuccess: (results) => processVideoThumbnails(results)
  })

  return (
    <div className="space-y-6">
      <section>
        <h3>Images</h3>
        <FileUploadSection {...images} accept="image/*" />
      </section>
      
      <section>
        <h3>Documents</h3>
        <FileUploadSection {...documents} accept=".pdf,.doc,.docx" />
      </section>
      
      <section>
        <h3>Videos</h3>
        <FileUploadSection {...videos} accept="video/*" />
      </section>
    </div>
  )
}
// Global configuration with per-route overrides
const upload = createUploadClient<AppRouter>({
  endpoint: '/api/upload',
  
  // These apply to all routes by default
  defaultOptions: {
    onProgress: (progress) => updateGlobalProgress(progress),
    onError: (error) => logError(error)
  }
})

export function MixedConfigUpload() {
  // Inherits global onProgress and onError
  const basic = upload.imageUpload()
  
  // Overrides global settings + adds success handler
  const premium = upload.documentUpload({
    endpoint: '/api/premium-upload', // Different endpoint
    onSuccess: (results) => {
      // This overrides global behavior
      handlePremiumUpload(results)
    }
    // Still inherits global onProgress and onError
  })

  return (
    <div>
      <FileUploader {...basic} label="Basic Images" />
      <FileUploader {...premium} label="Premium Documents" />
    </div>
  )
}
const upload = createUploadClient<AppRouter>({
  endpoint: '/api/upload',
  
  // Custom fetch function
  fetcher: async (input, init) => {
    const token = await getAuthToken()
    return fetch(input, {
      ...init,
  headers: {
        ...init?.headers,
        'Authorization': `Bearer ${token}`
      }
    })
  },
  
  defaultOptions: {
  onError: (error) => {
      // Global error tracking
      analytics.track('upload_error', { error: error.message })
      toast.error('Upload failed. Please try again.')
    }
  }
})

export function AdvancedUpload() {
  const { uploadFiles, files } = upload.secureUpload({
    endpoint: '/api/secure-upload',
    disabled: !user.hasPermission('upload'),
    onSuccess: (results) => {
      // Audit log for secure uploads
      auditLog('secure_upload_success', {
        files: results.length,
        user: user.id
      })
  }
})

  return <SecureFileUploader {...{ uploadFiles, files }} />
}

Type Safety Benefits

The structured client provides superior TypeScript integration:

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

// ✅ IntelliSense shows available routes
upload.imageUpload()      // Autocomplete suggests this
upload.documentUpload()   // And this
upload.videoUpload()      // And this

// ❌ TypeScript error for non-existent routes
upload.invalidRoute()     // Error: Property 'invalidRoute' does not exist

// ✅ Route rename safety
// If you rename 'imageUpload' to 'photoUpload' in your router,
// TypeScript will show errors everywhere it's used, making refactoring safe

// ✅ Callback type inference
upload.imageUpload({
  onSuccess: (results) => {
    // `results` is fully typed based on your router configuration
    results.forEach(result => {
      console.log(result.url)  // TypeScript knows this exists
      console.log(result.key)  // And this
    })
  }
})

Comparison with Hooks

FeatureEnhanced Structured ClientHook-Based
Type SafetySuperior - Property-based✅ Good - Generic types
IntelliSenseFull route autocomplete⚠️ String-based routes
RefactoringSafe rename across codebase⚠️ Manual find/replace
CallbacksFull support✅ Full support
Per-route ConfigFull support✅ Full support
Bundle SizeSame✅ Same
PerformanceIdentical✅ Identical

Migration from Hooks

Easy migration from hook-based approach:

// Before: Hook-based
import { useUploadRoute } from 'pushduck/client'

const { uploadFiles, files } = useUploadRoute<AppRouter>('imageUpload', {
  onSuccess: handleSuccess,
  onError: handleError
})

// After: Enhanced structured client
import { upload } from '@/lib/upload-client'

const { uploadFiles, files } = upload.imageUpload({
  onSuccess: handleSuccess,
  onError: handleError
})

Benefits of migration:

  • 🎯 Better type safety - Route names validated at compile time
  • 🔍 Enhanced IntelliSense - Autocomplete for all routes
  • 🏗️ Centralized config - Single place for endpoint and defaults
  • 🛡️ Refactoring safety - Rename routes safely
  • Same performance - Zero runtime overhead

Recommended Approach: Use createUploadClient for the best developer experience with full flexibility and type safety.