Server Router Configuration
Complete guide to configuring your upload router with type safety and advanced features
Server Router Configuration
The server router is the heart of pushduck. It defines your upload endpoints with complete type safety and validates all uploads against your schema.
This guide covers the enhanced router API with property-based client access. Looking for the legacy API? Check our migration guide.
Project Structure
A typical Next.js project with pushduck follows this structure:
Basic Router Setup
Create your upload route
Start by creating the API route that will handle your uploads:
import { s3 } from '@/lib/upload'
const s3Router = s3.createRouter({
imageUpload: s3
.image()
.max('4MB')
.count(10)
.middleware(async ({ req }) => {
// Add your authentication logic here
return { userId: "user_123" }
}),
documentUpload: s3
.file()
.max('10MB')
.types(['application/pdf'])
.count(1)
})
export const { POST, GET } = s3Router.handlers
Export router types
Create a separate file to export your router types for client-side usage:
import { createUploadConfig } from 'pushduck/server'
// Configure upload with provider and settings
const { s3, storage } = createUploadConfig()
.provider("cloudflareR2",{
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
region: 'auto',
endpoint: process.env.AWS_ENDPOINT_URL!,
bucket: process.env.S3_BUCKET_NAME!,
accountId: process.env.R2_ACCOUNT_ID!,
})
.build()
// Define your router
const s3Router = s3.createRouter({
imageUpload: s3.image().max('4MB').count(10),
documentUpload: s3.file().max('10MB').types(['application/pdf']).count(1)
})
// Export for use in API routes and client
export { s3, storage }
export type AppS3Router = typeof s3Router
Create typed client
Set up your client-side upload client with full type safety:
import { createUploadClient } from 'pushduck/client'
import type { AppS3Router } from './upload'
export const upload = createUploadClient<AppS3Router>({
endpoint: '/api/upload'
})
Schema Builder Reference
The s3
schema builders provide a fluent API for defining your upload requirements:
Prop | Type | Default |
---|---|---|
any? | FileConfig | - |
pdf? | FileConfig | - |
video? | FileConfig | - |
image? | FileConfig | - |
FileConfig Options
Prop | Type | Default |
---|---|---|
processing? | ProcessingConfig | - |
mimeTypes? | string[] | - |
maxCount? | number | 1 |
maxSize? | string | "1MB" |
Advanced Configuration
Multiple File Types
You can define schemas that accept multiple file types:
const s3Router = s3.createRouter({
mixedUpload: s3.object({
images: s3.image().max('4MB').count(5),
pdfs: s3.file().max('10MB').types(['application/pdf']).count(2),
documents: s3.file().max('5MB').types(['application/vnd.openxmlformats-officedocument.wordprocessingml.document']).count(3)
})
})
const s3Router = s3.createRouter({
mediaUpload: s3.object({
images: s3.image()
.max('4MB')
.count(10)
.formats(['jpeg', 'jpg', 'png', 'webp']),
videos: s3.file()
.max('100MB')
.count(2)
.types(['video/mp4', 'video/quicktime', 'video/avi'])
})
})
const s3Router = s3.createRouter({
genericUpload: s3.file()
.max('50MB')
.count(20)
.types([
'image/*', 'video/*', 'application/pdf',
'application/msword', 'text/plain'
])
})
Global Configuration
Configure settings that apply to all upload endpoints:
Prop | Type | Default |
---|---|---|
pathPrefix? | string | - |
uploadTimeout? | number | 300000 |
allowedOrigins? | string[] | ["*"] |
maxTotalSize? | string | "100MB" |
Deprecated: The pathPrefix
option is deprecated. Use the new hierarchical path configuration for better organization and flexibility.
// ❌ Deprecated approach - use modern createUploadConfig() instead
// This section is kept for reference only
For new projects, use the hierarchical path system instead:
// ✅ Modern approach with hierarchical paths
const { s3 } = createUploadConfig()
.provider("cloudflareR2",{ /* config */ })
.paths({
prefix: "uploads",
generateKey: (file, metadata) => {
return `${metadata.userId}/${Date.now()}/${file.name}`;
}
})
.build();
const s3Router = s3.createRouter({
images: s3.image().paths({ prefix: "images" }),
documents: s3.file().paths({ prefix: "documents" })
});
Multiple Providers
Support different storage providers for different upload types:
// ✅ Modern approach with multiple provider configurations
import { createUploadConfig } from "pushduck/server";
const primaryConfig = createUploadConfig()
.provider("cloudflareR2",{
// Primary R2 config for production files
accessKeyId: process.env.R2_ACCESS_KEY_ID!,
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
accountId: process.env.R2_ACCOUNT_ID!,
bucket: process.env.R2_BUCKET!,
})
.build();
const backupConfig = createUploadConfig()
.provider("aws",{
// AWS S3 config for backups
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
region: process.env.AWS_REGION!,
bucket: process.env.AWS_BACKUP_BUCKET!,
})
.build();
// Use primary config for main uploads
const s3Router = primaryConfig.s3.createRouter({
productImages: primaryConfig.s3.image().max("4MB").count(10),
// Backup handling would be done in lifecycle hooks
});
Middleware Integration
Add authentication, logging, and custom validation:
Authentication Middleware
const s3Router = s3.createRouter({
privateUploads: s3.image()
.max("4MB")
.count(5)
.middleware(async ({ req, metadata }) => {
const session = await getServerSession(req);
if (!session?.user?.id) {
throw new Error("Unauthorized");
}
return {
...metadata,
userId: session.user.id,
userRole: session.user.role,
};
}),
publicUploads: s3.image()
.max("1MB")
.count(1), // No middleware = publicly accessible
});
File Validation Middleware
import { z } from "zod";
const s3Router = s3.createRouter({
profilePicture: s3.image()
.max("2MB")
.count(1)
.middleware(async ({ req, file, metadata }) => {
// Custom file validation
if (file.name.includes("temp") || file.name.includes("test")) {
throw new Error("Temporary files not allowed");
}
const userId = await getUserId(req);
return { ...metadata, userId };
}),
});
Metadata Enhancement
const s3Router = s3.createRouter({
documentUpload: s3.file()
.max("10MB")
.count(1)
.types(["application/pdf"])
.middleware(async ({ req, metadata }) => {
const enrichedMetadata = {
...metadata,
uploadedAt: new Date().toISOString(),
userAgent: req.headers.get("user-agent"),
ip: req.headers.get("x-forwarded-for") || "unknown",
};
return enrichedMetadata;
}),
});
Lifecycle Hooks
Handle upload events for processing, notifications, and cleanup:
Prop | Type | Default |
---|---|---|
onUploadError? | (context, error) => void | Promise<void> | - |
onUploadComplete? | (context, result) => void | Promise<void> | - |
onUploadProgress? | (context, progress) => void | Promise<void> | - |
onUploadStart? | (context) => void | Promise<void> | - |
const s3Router = s3.createRouter({
imageUpload: s3.image()
.max("4MB")
.count(10)
.onUploadComplete(async ({ file, url, metadata }) => {
// Process uploaded image
await generateThumbnail(url);
await updateDatabase(file.key, metadata.userId);
})
.onUploadError(async ({ error, metadata }) => {
// Log errors and notify admins
console.error("Upload failed:", error);
await notifyAdmins(`Upload failed for user ${metadata.userId}`);
}),
});
Type Safety Features
Router Type Export
Export your router type for end-to-end type safety:
import { createUploadConfig } from "pushduck/server";
const { s3 } = createUploadConfig()
.provider("cloudflareR2",{ /* your config */ })
.build();
const s3Router = s3.createRouter({
// ... your configuration
});
export { s3 };
export type AppS3Router = typeof s3Router;
// Extract individual endpoint types
export type ImageUploadType = AppS3Router["imageUpload"];
export type DocumentUploadType = AppS3Router["documentUpload"];
Custom Context Types
Define custom context types for your middleware:
interface CustomContext {
userId: string;
userRole: "admin" | "user" | "guest";
organizationId?: string;
}
const s3Router = s3.createRouter({
upload: s3.image()
.max('4MB')
.count(5)
.middleware(async ({ req }): Promise<CustomContext> => {
// Your auth logic here
return {
userId: "user_123",
userRole: "user",
};
}),
});
Real-World Examples
E-commerce Product Images
const ecommerceRouter = s3.createRouter({
productImages: s3.image()
.max('5MB')
.count(8)
.formats(['webp', 'jpeg'])
.middleware(async ({ req }) => {
const vendorId = await getVendorId(req);
return { vendorId, category: "products" };
})
.onUploadComplete(async ({ files, metadata }) => {
// Update product catalog
await updateProductImages(metadata.vendorId, files);
}),
});
Document Management System
const docsRouter = s3.createRouter({
contracts: s3.file()
.max('25MB')
.types(['application/pdf'])
.count(1)
.middleware(async ({ req }) => {
const { userId, companyId } = await validateContractUpload(req);
return { userId, companyId, confidential: true };
}),
proposals: s3.object({
pdfs: s3.file().max('50MB').types(['application/pdf']).count(3),
documents: s3.file().max('25MB').types(['application/vnd.openxmlformats-officedocument.wordprocessingml.document']).count(5),
}).middleware(async ({ req }) => {
const { userId, projectId } = await validateProposalUpload(req);
return { userId, projectId };
}),
});
Social Media Platform
export const socialRouter = createUploadRouter({
profilePicture: uploadSchema({
image: {
maxSize: "2MB",
maxCount: 1,
processing: {
resize: { width: 400, height: 400 },
format: "webp",
},
},
}),
postMedia: uploadSchema({
image: { maxSize: "8MB", maxCount: 4 },
video: { maxSize: "100MB", maxCount: 1 },
}).middleware(async ({ req }) => {
const userId = await authenticateUser(req);
return { userId, postType: "media" };
}),
});
Security Best Practices
Important: Always implement proper authentication and file validation in production environments.
Content Type Validation
export const router = createUploadRouter({
secureUpload: uploadSchema({
image: {
maxSize: "4MB",
maxCount: 5,
mimeTypes: ["image/jpeg", "image/png", "image/webp"], // Explicit whitelist
},
}).middleware(async ({ req, files }) => {
// Additional security checks
for (const file of files) {
// Validate file headers match content type
const isValidImage = await validateImageFile(file);
if (!isValidImage) {
throw new Error("Invalid image file");
}
}
return { userId: await getUserId(req) };
}),
});
Rate Limiting
import { ratelimit } from "@/lib/ratelimit";
export const router = createUploadRouter({
upload: uploadSchema({
any: { maxSize: "10MB", maxCount: 3 },
}).middleware(async ({ req }) => {
const ip = req.headers.get("x-forwarded-for") || "unknown";
const { success } = await ratelimit.limit(ip);
if (!success) {
throw new Error("Rate limit exceeded");
}
return { ip };
}),
});
Next Steps: Now that you have your router configured, learn how to configure your client for the best developer experience.