Getting Started

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:

.env.local
# 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.

.env.local
# 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:

🎨 Enhanced UI

Add drag & drop, progress bars, and beautiful components

Image Upload Guide →

🔒 Custom Validation

Add authentication, custom metadata, and middleware

Router Configuration →

☁️ Other Providers

Try Cloudflare R2 for better performance, or AWS S3, DigitalOcean, MinIO

Provider Setup →

⚡ Enhanced Client

Upgrade to property-based access for better DX

Migration Guide →

Need Help?

Loving Pushduck? Give us a ⭐ on GitHub and help spread the word!