Quick Start
Get production-ready file uploads working in your Next.js app in under 2 minutes.
Quick Start
Get production-ready file uploads working in your Next.js app in under 2 minutes with our CLI tool. Interactive setup, just one command.
🚀 New! Use our CLI for instant setup: npx @pushduck/cli@latest init
- handles everything automatically!
Choose Your Setup Method
⚡ Interactive CLI Setup
Get everything set up instantly with our interactive CLI:
npx @pushduck/cli@latest init
That's it! The CLI will:
- ✅ Install dependencies automatically
- ✅ Set up your chosen provider (AWS S3, Cloudflare R2, etc.)
- ✅ Create API routes with type safety
- ✅ Generate example components
- ✅ Configure environment variables
- ✅ Create and configure your S3 bucket
What you get:
- Production-ready upload API in
app/api/upload/route.ts
- Type-safe upload client in
lib/upload-client.ts
- Example components in
components/ui/
- Working demo page in
app/upload/page.tsx
Example CLI Output:
┌─────────────────────────────────────────────────────────────┐
│ │
│ 🚀 Welcome to Pushduck │
│ │
│ Let's get your file uploads working in 2 minutes! │
│ │
└─────────────────────────────────────────────────────────────┘
🔍 Detecting your project...
✓ Next.js App Router detected
✓ TypeScript configuration found
? Which cloud storage provider would you like to use?
❯ AWS S3 (recommended)
Cloudflare R2 (S3-compatible, global edge)
DigitalOcean Spaces (simple, affordable)
✨ Generated files:
├── app/api/upload/route.ts
├── app/upload/page.tsx
├── components/ui/upload-button.tsx
├── lib/upload-client.ts
└── .env.example
🎉 Setup complete! Your uploads are ready.
🔧 Manual Setup
If you prefer to set things up manually or need custom configuration:
Prerequisites
- Next.js 13+ with App Router
- An S3-compatible storage provider (we'll use AWS S3 in this guide)
- Node.js 18+
Install Pushduck
npm install pushduck
Using a different package manager?
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 S3 credentials:
# .env.local
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.
Create Your Upload Router
Create an API route to handle file uploads:
// app/api/s3-upload/route.ts
import { createUploadConfig } from "pushduck/server";
const { s3 } = createUploadConfig()
.provider("cloudflareR2",{
accountId: process.env.CLOUDFLARE_ACCOUNT_ID!,
accessKeyId: process.env.CLOUDFLARE_ACCESS_KEY_ID!,
secretAccessKey: process.env.CLOUDFLARE_SECRET_ACCESS_KEY!,
bucket: process.env.CLOUDFLARE_BUCKET_NAME!,
region: "auto",
})
.build();
const router = s3.createRouter({
// Define your upload routes with validation
imageUpload: s3
.image()
.max("10MB")
.formats(["jpeg", "png", "webp"]),
documentUpload: s3.file().max("50MB").types(["application/pdf", "application/msword"]),
});
export { router as POST };
// Export the router type for client-side type safety
export type Router = typeof router;
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 using the recommended structured approach:
Recommended: The structured client provides the best developer experience with property-based access, centralized configuration, and enhanced type safety.
// lib/upload-client.ts
import { createUploadClient } from "pushduck/client";
import type { Router } from "@/app/api/s3-upload/route";
// Create a type-safe upload client (recommended)
export const upload = createUploadClient<Router>({
endpoint: "/api/s3-upload",
});
Why this approach is recommended:
- ✅ Full type inference from your server router
- ✅ Property-based access -
upload.imageUpload()
instead of strings - ✅ IntelliSense support - see all available endpoints
- ✅ Refactoring safety - rename routes with confidence
- ✅ Centralized config - set headers, timeouts, and options once
Alternative: You can also use the hook-based approach if you prefer traditional React patterns:
// With type parameter (recommended)
const { uploadFiles } = useUploadRoute<AppRouter>('imageUpload')
// Or without type parameter (also works)
const { uploadFiles } = useUploadRoute('imageUpload')
The structured client is still recommended for most use cases.
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
What's Next?
Now that you have the basics working, explore these advanced features:
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!