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
import { createUploadClient } from 'pushduck/client'
import type { AppRouter } from './upload'
export const upload = createUploadClient<AppRouter>({
endpoint: '/api/upload'
})
Use in components
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
Prop | Type | Default |
---|---|---|
fetcher? | (input: RequestInfo, init?: RequestInit) => Promise<Response> | - |
defaultOptions? | RouteUploadOptions | - |
endpoint | string | - |
Per-Route Configuration
Each route method now accepts optional configuration:
Prop | Type | Default |
---|---|---|
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
Feature | Enhanced Structured Client | Hook-Based |
---|---|---|
Type Safety | ✅ Superior - Property-based | ✅ Good - Generic types |
IntelliSense | ✅ Full route autocomplete | ⚠️ String-based routes |
Refactoring | ✅ Safe rename across codebase | ⚠️ Manual find/replace |
Callbacks | ✅ Full support | ✅ Full support |
Per-route Config | ✅ Full support | ✅ Full support |
Bundle Size | ✅ Same | ✅ Same |
Performance | ✅ Identical | ✅ 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.