Manual Setup
Step-by-step manual setup for developers who prefer full control over configuration
Prerequisites
- Next.js 13+ with App Router
- An S3-compatible storage provider (we recommend Cloudflare R2 for best performance and cost)
- Node.js 18+
Install Pushduck
npm install pushduck
pnpm add pushduck
yarn add pushduck
bun add pushduck
Set Environment Variables
Create a .env.local
file in your project root with your storage credentials:
# Cloudflare R2 Configuration
CLOUDFLARE_R2_ACCESS_KEY_ID=your_access_key
CLOUDFLARE_R2_SECRET_ACCESS_KEY=your_secret_key
CLOUDFLARE_R2_ACCOUNT_ID=your_account_id
CLOUDFLARE_R2_BUCKET_NAME=your-bucket-name
Don't have R2 credentials yet? Follow our Cloudflare R2 setup guide to create a bucket and get your credentials in 2 minutes.
# AWS S3 Configuration
AWS_ACCESS_KEY_ID=your_access_key
AWS_SECRET_ACCESS_KEY=your_secret_key
AWS_REGION=us-east-1
AWS_S3_BUCKET_NAME=your-bucket-name
Don't have S3 credentials yet? Follow our AWS S3 setup guide to create a bucket and get your credentials in 2 minutes.
Configure Upload Settings
First, create your upload configuration:
// lib/upload.ts
import { createUploadConfig } from "pushduck/server";
// Configure your S3-compatible storage
export const { s3, storage } = createUploadConfig()
.provider("cloudflareR2",{
accessKeyId: process.env.CLOUDFLARE_R2_ACCESS_KEY_ID!,
secretAccessKey: process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY!,
region: "auto",
endpoint: `https://${process.env.CLOUDFLARE_R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
bucket: process.env.CLOUDFLARE_R2_BUCKET_NAME!,
accountId: process.env.CLOUDFLARE_R2_ACCOUNT_ID!,
})
.build();
Create Your Upload Router
Create an API route to handle file uploads:
// app/api/s3-upload/route.ts
import { s3 } from "@/lib/upload";
const s3Router = s3.createRouter({
// Define your upload routes with validation
imageUpload: s3
.image()
.max("10MB")
.formats(["jpg", "jpeg", "png", "webp"]),
documentUpload: s3.file().max("50MB").types(["application/pdf", "application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"]),
});
export const { GET, POST } = s3Router.handlers;
// Export the router type for client-side type safety
export type Router = typeof s3Router;
What's happening here? - s3.createRouter()
creates a type-safe upload
handler - s3.image()
and s3.file()
provide validation and TypeScript
inference - The router automatically handles presigned URLs, validation, and
errors - Exporting the type enables full client-side type safety
Create Upload Client
Create a type-safe client for your components:
// lib/upload-client.ts
import { createUploadClient } from "pushduck";
import type { Router } from "@/app/api/s3-upload/route";
// Create a type-safe upload client
export const upload = createUploadClient<Router>({
baseUrl: "/api/s3-upload",
});
// You can also export specific upload methods
export const { imageUpload, documentUpload } = upload;
Use in Your Components
Now you can use the upload client in any component with full type safety:
// components/image-uploader.tsx
"use client";
import { upload } from "@/lib/upload-client";
export function ImageUploader() {
const { uploadFiles, uploadedFiles, isUploading, progress, error } =
upload.imageUpload();
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (files) {
uploadFiles(Array.from(files));
}
};
return (
<div className="space-y-4">
<div className="flex items-center space-x-4">
<input
type="file"
multiple
accept="image/*"
onChange={handleFileChange}
disabled={isUploading}
className="file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100"
/>
{isUploading && (
<div className="flex items-center space-x-2">
<div className="w-4 h-4 border-2 border-blue-600 border-t-transparent rounded-full animate-spin" />
<span className="text-sm text-gray-600">
Uploading... {Math.round(progress)}%
</span>
</div>
)}
</div>
{error && (
<div className="p-3 bg-red-50 border border-red-200 rounded-md">
<p className="text-sm text-red-600">{error.message}</p>
</div>
)}
{uploadedFiles.length > 0 && (
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
{uploadedFiles.map((file) => (
<div key={file.key} className="space-y-2">
<img
src={file.url}
alt="Uploaded image"
className="w-full h-32 object-cover rounded-lg border"
/>
<p className="text-xs text-gray-500 truncate">{file.name}</p>
</div>
))}
</div>
)}
</div>
);
}
Add to Your Page
Finally, use your upload component in any page:
// app/page.tsx
import { ImageUploader } from "@/components/image-uploader";
export default function HomePage() {
return (
<div className="max-w-2xl mx-auto p-8">
<h1 className="text-3xl font-bold mb-8">Upload Images</h1>
<ImageUploader />
</div>
);
}
🎉 Congratulations!
You now have production-ready file uploads working in your Next.js app! Here's what you accomplished:
- ✅ Type-safe uploads with full TypeScript inference
- ✅ Automatic validation for file types and sizes
- ✅ Progress tracking with loading states
- ✅ Error handling with user-friendly messages
- ✅ Secure uploads using presigned URLs
- ✅ Multiple file support with image preview
Turbo Mode Issue: If you're using next dev --turbo
and experiencing upload issues, try removing the --turbo
flag from your dev script. There's a known compatibility issue with Turbo mode that can affect file uploads.
What's Next?
Now that you have the basics working, explore these advanced features:
☁️ Other Providers
Try Cloudflare R2 for better performance, or AWS S3, DigitalOcean, MinIO
Provider Setup →
Need Help?
- 📖 Documentation: Explore our comprehensive guides
- 💬 Community: Join our Discord community
- 🐛 Issues: Report bugs on GitHub
- 📧 Support: Email us at support@pushduck.com
Loving Pushduck? Give us a ⭐ on GitHub and help spread the word!