API Reference
S3 Router
Type-safe upload routes with schema validation and middleware
S3 Router
The S3 router provides a type-safe way to define upload endpoints with schema validation, middleware, and lifecycle hooks.
Basic Router Setup
// app/api/upload/route.ts
import { s3 } from '@/lib/upload'
const s3Router = s3.createRouter({
imageUpload: s3
.image()
.max('5MB')
.formats(['jpeg', 'jpg', 'png', 'webp'])
.middleware(async ({ file, metadata }) => {
// Add authentication and user context
return {
...metadata,
userId: 'user-123',
uploadedAt: new Date().toISOString(),
}
}),
documentUpload: s3
.file()
.max('10MB')
.types(['application/pdf', 'text/plain'])
.paths({
prefix: 'documents',
}),
})
// Export the handler
export const { GET, POST } = s3Router.handlers;
Schema Builders
Image Schema
s3.image()
.max('5MB')
.formats(['jpeg', 'jpg', 'png', 'webp', 'gif'])
.dimensions({ minWidth: 100, maxWidth: 2000 })
.quality(0.8) // JPEG quality
File Schema
s3.file()
.max('10MB')
.types(['application/pdf', 'text/plain', 'application/json'])
.extensions(['pdf', 'txt', 'json'])
Object Schema (Multiple Files)
s3.object({
images: s3.image().max('5MB').count(5),
documents: s3.file().max('10MB').count(2),
thumbnail: s3.image().max('1MB').count(1),
})
Route Configuration
Middleware
Add authentication, validation, and metadata:
.middleware(async ({ file, metadata, req }) => {
// Authentication
const user = await authenticateUser(req)
if (!user) {
throw new Error('Authentication required')
}
// File validation
if (file.size > 10 * 1024 * 1024) {
throw new Error('File too large')
}
// Return enriched metadata
return {
...metadata,
userId: user.id,
userRole: user.role,
uploadedAt: new Date().toISOString(),
ipAddress: req.headers.get('x-forwarded-for'),
}
})
Path Configuration
Control where files are stored:
.paths({
// Simple prefix
prefix: 'user-uploads',
// Custom path generation
generateKey: (ctx) => {
const { file, metadata, routeName } = ctx
const userId = metadata.userId
const timestamp = Date.now()
return `${routeName}/${userId}/${timestamp}/${file.name}`
},
// Simple suffix
suffix: 'processed',
})
Lifecycle Hooks
React to upload events:
.onUploadStart(async ({ file, metadata }) => {
console.log(`Starting upload: ${file.name}`)
// Log to analytics
await analytics.track('upload_started', {
userId: metadata.userId,
filename: file.name,
fileSize: file.size,
})
})
.onUploadComplete(async ({ file, url, metadata }) => {
console.log(`Upload complete: ${file.name} -> ${url}`)
// Save to database
await db.files.create({
filename: file.name,
url,
userId: metadata.userId,
size: file.size,
contentType: file.type,
uploadedAt: new Date(),
})
// Send notification
await notificationService.send({
userId: metadata.userId,
type: 'upload_complete',
message: `${file.name} uploaded successfully`,
})
})
.onUploadError(async ({ file, error, metadata }) => {
console.error(`Upload failed: ${file.name}`, error)
// Log error
await errorLogger.log({
operation: 'file_upload',
error: error.message,
userId: metadata.userId,
filename: file.name,
})
})
Advanced Examples
E-commerce Product Images
const productRouter = s3.createRouter({
productImages: s3
.image()
.max('5MB')
.formats(['jpeg', 'jpg', 'png', 'webp'])
.dimensions({ minWidth: 800, maxWidth: 2000 })
.middleware(async ({ metadata, req }) => {
const user = await authenticateUser(req)
const productId = metadata.productId
// Verify user owns the product
const product = await db.products.findFirst({
where: { id: productId, ownerId: user.id }
})
if (!product) {
throw new Error('Product not found or access denied')
}
return {
...metadata,
userId: user.id,
productId,
productName: product.name,
}
})
.paths({
generateKey: (ctx) => {
const { metadata } = ctx
return `products/${metadata.productId}/images/${Date.now()}.jpg`
}
})
.onUploadComplete(async ({ url, metadata }) => {
// Update product with new image
await db.products.update({
where: { id: metadata.productId },
data: {
images: {
push: url
}
}
})
}),
productDocuments: s3
.file()
.max('10MB')
.types(['application/pdf'])
.paths({
prefix: 'product-docs',
})
.onUploadComplete(async ({ url, metadata }) => {
await db.productDocuments.create({
productId: metadata.productId,
documentUrl: url,
type: 'specification',
})
}),
})
User Profile System
const profileRouter = s3.createRouter({
avatar: s3
.image()
.max('2MB')
.formats(['jpeg', 'jpg', 'png'])
.dimensions({ minWidth: 100, maxWidth: 500 })
.middleware(async ({ req }) => {
const user = await authenticateUser(req)
return { userId: user.id, type: 'avatar' }
})
.paths({
generateKey: (ctx) => {
return `users/${ctx.metadata.userId}/avatar.jpg`
}
})
.onUploadComplete(async ({ url, metadata }) => {
// Update user profile
await db.users.update({
where: { id: metadata.userId },
data: { avatarUrl: url }
})
// Invalidate cache
await cache.del(`user:${metadata.userId}`)
}),
documents: s3
.object({
resume: s3.file().max('5MB').types(['application/pdf']).count(1),
portfolio: s3.file().max('10MB').count(3),
})
.middleware(async ({ req }) => {
const user = await authenticateUser(req)
return { userId: user.id }
})
.paths({
prefix: 'user-documents',
}),
})
Client-Side Usage
Once you have your router set up, use it from the client:
// components/FileUploader.tsx
import { useUploadRoute } from 'pushduck'
export function FileUploader() {
const { upload, isUploading } = useUploadRoute('imageUpload')
const handleUpload = async (files: FileList) => {
try {
const results = await upload(files, {
// This metadata will be passed to middleware
productId: 'product-123',
category: 'main-images',
})
console.log('Upload complete:', results)
} catch (error) {
console.error('Upload failed:', error)
}
}
return (
<div>
<input
type="file"
multiple
accept="image/*"
onChange={(e) => e.target.files && handleUpload(e.target.files)}
disabled={isUploading}
/>
{isUploading && <p>Uploading...</p>}
</div>
)
}
Type Safety
The router provides full TypeScript support:
// Types are automatically inferred
type RouterType = typeof s3Router
// Get route names
type RouteNames = keyof RouterType // 'imageUpload' | 'documentUpload'
// Get route input types
type ImageUploadInput = InferRouteInput<RouterType['imageUpload']>
// Get route metadata types
type ImageUploadMetadata = InferRouteMetadata<RouterType['imageUpload']>