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:
Basic Client Configuration
Import your router types
Start by importing the router type from your server configuration:
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:
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 | Default |
---|---|---|
onError? | (error: UploadError) => void | - |
onProgress? | (progress: UploadProgress) => void | - |
retries? | number | 3 |
timeout? | number | 30000 |
headers? | Record<string, string> | - |
endpoint | string | - |
Advanced Client Configuration
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 | Default |
---|---|---|
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:
Prop | Type | Default |
---|---|---|
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:
Prop | Type | Default |
---|---|---|
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.