LogoBoring Template

Image Upload

Upload files and images to AWS S3 using pre-signed URLs for secure uploads in your application.

This template uses AWS S3 for image uploading. For more information, visit the AWS S3 documentation.

Setup

Prerequisites

AWS Account

☁️ Account with S3 access permissions

S3 Bucket

🪣 Configured S3 bucket for storage

AWS Credentials

🔑 Access and secret keys configured

Environment Variables

Add the following variables to your .env.local file:

.env.local
# AWS Configuration
AWS_ACCESS_KEY_ID=your_access_key_id
AWS_SECRET_ACCESS_KEY=your_secret_access_key
AWS_REGION=your_region
S3_UPLOAD_BUCKET=your_bucket_name

Never commit your AWS credentials to version control. Always use environment variables.

Components

Image Upload Component

A reusable component for handling image uploads with preview:

components/image-upload.tsx
type ImageUploadProps = {
  value: string | null | undefined;
  onChange: (url?: string, name?: string) => void;
  type?: "profile" | "logo";
  disabled?: boolean;
  disableActions?: boolean;
};
 
export function ImageUpload({
  value,
  onChange,
  type = "profile",
  disabled = false,
  disableActions = false,
}: ImageUploadProps) {
  const [isUploading, setIsUploading] = useState<boolean>(false);
 
  const handleFileDrop = async (file: File) => {
    try {
      setIsUploading(true);
      const data = await getPreSignedUrl(file);
 
      if (!data?.data) {
        return toast.error("Too many requests");
      }
 
      const uploadSuccessful = await uploadFileToS3(
        data.data.preSignedUrl,
        file
      );
 
      if (uploadSuccessful) {
        onChange(data.data.imageUrl, file.name);
      }
    } catch (error) {
      toast.error(error);
    } finally {
      setIsUploading(false);
    }
  };
 
  // ... rest of component
}

Features

Drag & Drop

🖱️ Support for drag and drop file uploads using react-dropzone

Image Preview

🖼️ Real-time preview of uploaded images with Next.js Image component

File Validation

✅ Validates file types and shows appropriate error messages

Loading States

⏳ Visual feedback during upload process with loading indicators

Upload Process

The image upload process uses pre-signed URLs to securely upload files directly to S3 without exposing your AWS credentials.

1. Get Pre-signed URL

Request a pre-signed URL from the server:

lib/image-upload.ts
async function getPreSignedUrl(file: File) {
  try {
    const res = await fetch("/api/image-upload", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        filename: file.name,
        filetype: file.type,
      }),
    });
 
    if (!res.ok) {
      throw new Error((await res.json()).message);
    }
 
    return await res.json();
  } catch (error) {
    throw Error(error?.message);
  }
}

2. Upload to S3

Upload the file directly to S3 using the pre-signed URL:

lib/image-upload.ts
export const uploadFileToS3 = async (url: string, file: File) => {
  try {
    const buffer = await file.arrayBuffer();
 
    await new Promise<void>((resolve, reject) => {
      const xhr = new XMLHttpRequest();
      xhr.open("PUT", url, true);
      xhr.setRequestHeader("Content-Type", file.type);
      xhr.setRequestHeader("Cache-Control", "max-age=63072000");
 
      xhr.onreadystatechange = function () {
        if (xhr.readyState === 4) {
          xhr.status >= 200 && xhr.status < 300 ? resolve() : reject();
        }
      };
 
      xhr.send(buffer);
    });
 
    return true;
  } catch (error) {
    return false;
  }
};

API Route

The /api/image-upload route handles the server-side logic for generating pre-signed URLs and managing S3 uploads.

API Route Handler

Server-side implementation for secure image uploads:

app/api/image-upload/route.ts
export async function POST(req: NextRequest) {
  try {
    // 1. Get request data and authenticate user
    const { filename, filetype } = await req.json();
    const { user } = await getCurrentUser();
 
    if (!user) {
      return responses.notAuthenticatedResponse();
    }
 
    // 2. Check rate limit
    const identifier = `ratelimit:image-upload:${user.id}`;
    const { success } = await ratelimit.limit(identifier);
 
    if (!success) {
      return responses.tooManyRequestsResponse();
    }
 
    // 3. Generate unique filename
    const generatedFilename = getFilename(filename);
 
    // 4. Configure S3 parameters
    const params = {
      Bucket: process.env.S3_UPLOAD_BUCKET,
      Key: `${user.id}/${generatedFilename}`,
      ContentType: filetype,
      CacheControl: "max-age=63072000",
    };
 
    // 5. Generate pre-signed URL
    const preSignedUrl = await getSignedUrl(
      awsClient,
      new PutObjectCommand(params),
      { expiresIn: 60 * 60 }
    );
 
    // 6. Create the final image URL
    const imageUrl = `https://${process.env.S3_UPLOAD_BUCKET}.s3.amazonaws.com/${user.id}/${generatedFilename}`;
 
    return responses.successResponse(
      { preSignedUrl, imageUrl },
      "Image uploaded successfully"
    );
  } catch (error: any) {
    return responses.internalServerErrorResponse(error.message);
  }
}

Key Features

Secure File Names

🔒 Generates unique file names using MD5 hashing and timestamps

Rate Limiting

⏱️ Prevents abuse with Upstash Redis-based rate limiting

User Isolation

👤 Stores files in user-specific S3 directories

Caching Strategy

📦 Implements proper cache control headers

Filename Generation

Secure filename generation to prevent conflicts:

lib/image-upload.ts
const getFilename = (originalName: string) => {
  const originalExtension = path.extname(originalName);
  const currentTime = new Date().getTime().toString();
  const hash = crypto.createHash("md5").update(currentTime).digest("hex");
  return `${hash}${originalExtension}`;
};

Rate Limiting Setup

Configure rate limiting with Upstash Redis:

const ratelimit = new Ratelimit({
  redis,
  limiter: Ratelimit.slidingWindow(RATE_LIMIT_5, RATE_LIMIT_1_MINUTE),
});

AWS Configuration

AWS Client Setup

Configure the AWS S3 client:

lib/aws.ts
import { S3Client } from "@aws-sdk/client-s3";
 
export const awsClient = new S3Client({
  credentials: {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
  },
  region: process.env.AWS_REGION,
});

The S3Client is configured using environment variables to keep credentials secure.

Usage Example

Here's how to implement the image upload component in a form:

app/(dashboard)/settings/profile/page.tsx
import { ImageUpload } from "@/components/image-upload";
import { Button } from "@/components/ui/button";
import { Form } from "@/components/ui/form";
 
export default function ProfilePage() {
  const form = useForm({
    defaultValues: {
      avatar: user?.image || "",
    },
  });
 
  return (
    <Form {...form}>
      <div className="space-y-4">
        <div className="flex justify-center">
          <ImageUpload
            value={form.watch("avatar")}
            onChange={(url) => form.setValue("avatar", url)}
            type="profile"
          />
        </div>
        <Button type="submit">Update Profile</Button>
      </div>
    </Form>
  );
}

CORS Configuration

Important

You must configure CORS settings on your S3 bucket to allow uploads from your domain.

Add the following CORS configuration to your S3 bucket:

S3 CORS Configuration
[
  {
    "AllowedHeaders": ["*"],
    "AllowedMethods": ["PUT", "POST", "GET"],
    "AllowedOrigins": ["https://your-domain.com", "http://localhost:3000"],
    "ExposeHeaders": []
  }
]

Best Practices

Security

  • Use pre-signed URLs for secure uploads
  • Implement proper file validation
  • Set appropriate CORS policies
  • Use environment variables for credentials

User Experience

  • Show upload progress indicators
  • Provide clear error messages
  • Support drag and drop
  • Preview uploaded images

Additional Resources