Use Cases

How to Implement File Upload with Claude Code

Learn how to implement file upload using Claude Code. Includes practical code examples and step-by-step guidance.

Using Claude Code to Implement File Uploads

File upload features involve a lot of moving parts: validation, presigned URLs, progress display, image resizing, and more. By delegating all of this to Claude Code in one pass, you can efficiently build a secure and user-friendly upload feature.

Direct Upload With a Presigned URL

> Implement a file upload that uses S3 presigned URLs.
> The client should upload directly to S3 without going through our server.
> Max file size 10MB, only allow images and PDFs.

Server Side (Issuing the Presigned URL)

// src/app/api/upload/route.ts
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { NextRequest, NextResponse } from 'next/server';
import { nanoid } from 'nanoid';

const s3 = new S3Client({ region: 'us-east-1' });

const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'application/pdf'];
const MAX_SIZE = 10 * 1024 * 1024; // 10MB

export async function POST(req: NextRequest) {
  const { fileName, fileType, fileSize } = await req.json();

  // Validation
  if (!ALLOWED_TYPES.includes(fileType)) {
    return NextResponse.json(
      { error: 'File type not allowed' },
      { status: 400 }
    );
  }

  if (fileSize > MAX_SIZE) {
    return NextResponse.json(
      { error: 'File size must be 10MB or less' },
      { status: 400 }
    );
  }

  const ext = fileName.split('.').pop();
  const key = `uploads/${nanoid()}.${ext}`;

  const command = new PutObjectCommand({
    Bucket: process.env.S3_BUCKET!,
    Key: key,
    ContentType: fileType,
    ContentLength: fileSize,
  });

  const signedUrl = await getSignedUrl(s3, command, { expiresIn: 300 });

  return NextResponse.json({
    uploadUrl: signedUrl,
    fileUrl: `https://${process.env.S3_BUCKET}.s3.us-east-1.amazonaws.com/${key}`,
    key,
  });
}

Client Side (Upload With Progress)

import { useState, useCallback } from 'react';

interface UploadState {
  progress: number;
  uploading: boolean;
  error: string | null;
  fileUrl: string | null;
}

export function useFileUpload() {
  const [state, setState] = useState<UploadState>({
    progress: 0,
    uploading: false,
    error: null,
    fileUrl: null,
  });

  const upload = useCallback(async (file: File) => {
    setState({ progress: 0, uploading: true, error: null, fileUrl: null });

    try {
      // 1. Get the presigned URL
      const res = await fetch('/api/upload', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          fileName: file.name,
          fileType: file.type,
          fileSize: file.size,
        }),
      });

      if (!res.ok) {
        const { error } = await res.json();
        throw new Error(error);
      }

      const { uploadUrl, fileUrl } = await res.json();

      // 2. Upload directly to S3 (with progress)
      await new Promise<void>((resolve, reject) => {
        const xhr = new XMLHttpRequest();

        xhr.upload.addEventListener('progress', (e) => {
          if (e.lengthComputable) {
            setState(prev => ({ ...prev, progress: Math.round((e.loaded / e.total) * 100) }));
          }
        });

        xhr.addEventListener('load', () => {
          if (xhr.status >= 200 && xhr.status < 300) resolve();
          else reject(new Error('Upload failed'));
        });

        xhr.addEventListener('error', () => reject(new Error('Network error')));

        xhr.open('PUT', uploadUrl);
        xhr.setRequestHeader('Content-Type', file.type);
        xhr.send(file);
      });

      setState({ progress: 100, uploading: false, error: null, fileUrl });
      return fileUrl;
    } catch (err) {
      setState(prev => ({
        ...prev,
        uploading: false,
        error: (err as Error).message,
      }));
      throw err;
    }
  }, []);

  return { ...state, upload };
}

Uploader Component

function FileUploader() {
  const { progress, uploading, error, fileUrl, upload } = useFileUpload();
  const [dragOver, setDragOver] = useState(false);

  const handleDrop = useCallback(async (e: React.DragEvent) => {
    e.preventDefault();
    setDragOver(false);
    const file = e.dataTransfer.files[0];
    if (file) await upload(file);
  }, [upload]);

  const handleFileSelect = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (file) await upload(file);
  }, [upload]);

  return (
    <div>
      <div
        onDragOver={(e) => { e.preventDefault(); setDragOver(true); }}
        onDragLeave={() => setDragOver(false)}
        onDrop={handleDrop}
        className={`border-2 border-dashed rounded-lg p-8 text-center transition-colors
          ${dragOver ? 'border-blue-500 bg-blue-50' : 'border-gray-300'}
          ${uploading ? 'pointer-events-none opacity-50' : 'cursor-pointer'}`}
      >
        <p className="text-gray-600">
          Drag and drop files, or click to select
        </p>
        <p className="mt-1 text-sm text-gray-400">
          JPEG, PNG, WebP, PDF (10MB or less)
        </p>
        <input
          type="file"
          accept="image/jpeg,image/png,image/webp,application/pdf"
          onChange={handleFileSelect}
          className="hidden"
          id="file-input"
        />
        <label htmlFor="file-input" className="mt-4 inline-block cursor-pointer rounded bg-blue-600 px-4 py-2 text-white">
          Select file
        </label>
      </div>

      {uploading && (
        <div className="mt-4">
          <div className="h-2 rounded-full bg-gray-200">
            <div
              className="h-full rounded-full bg-blue-600 transition-all"
              style={{ width: `${progress}%` }}
            />
          </div>
          <p className="mt-1 text-sm text-gray-500">{progress}%</p>
        </div>
      )}

      {error && <p className="mt-2 text-sm text-red-600">{error}</p>}

      {fileUrl && (
        <p className="mt-2 text-sm text-green-600">
          Upload complete
        </p>
      )}
    </div>
  );
}

Image Resize Processing

You can also ask Claude Code to implement automatic image resizing in Lambda for uploaded images.

// lambda/resize-image.ts
import { S3Event } from 'aws-lambda';
import { S3Client, GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';
import sharp from 'sharp';

const s3 = new S3Client({});

const SIZES = [
  { suffix: 'thumb', width: 200, height: 200 },
  { suffix: 'medium', width: 800 },
  { suffix: 'large', width: 1600 },
];

export async function handler(event: S3Event) {
  for (const record of event.Records) {
    const bucket = record.s3.bucket.name;
    const key = decodeURIComponent(record.s3.object.key);

    const { Body } = await s3.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
    const buffer = Buffer.from(await Body!.transformToByteArray());

    for (const size of SIZES) {
      const resized = await sharp(buffer)
        .resize(size.width, size.height, { fit: 'cover' })
        .webp({ quality: 80 })
        .toBuffer();

      const resizedKey = key.replace(/\.[^.]+$/, `-${size.suffix}.webp`);
      await s3.send(new PutObjectCommand({
        Bucket: bucket,
        Key: resizedKey,
        Body: resized,
        ContentType: 'image/webp',
      }));
    }
  }
}

Summary

With Claude Code, you can efficiently implement file upload features including presigned URLs, progress display, validation, and image resizing. Its strength is generating a security-aware implementation all at once. For AWS integration details, see automating AWS deployments. For using it in personal projects, see how to turbocharge personal development.

For Claude Code details, see the official Anthropic documentation.

#Claude Code #file upload #S3 #React #Node.js

Level up your Claude Code workflow

50 battle-tested prompt templates you can copy-paste into Claude Code right now.

Free

Free PDF: Claude Code Cheatsheet in 5 Minutes

Key commands, shortcuts, and prompt examples on a single printable page.

Download PDF
M

About the Author

Masa

Engineer obsessed with Claude Code. Runs claudecode-lab.com, a 10-language tech media with 2,000+ pages.