DEV Community

Atlas Whoff
Atlas Whoff

Posted on

Handling File Uploads: S3, Presigned URLs, and Direct Browser Uploads

Handling File Uploads: S3, Presigned URLs, and Direct Browser Uploads

The naive approach — upload to your server, then to S3 — doubles bandwidth cost and adds latency. The right pattern bypasses your server entirely.

The Wrong Way

Browser → Your Server → S3
Enter fullscreen mode Exit fullscreen mode

Your server becomes a bottleneck. Large files eat memory. You're paying for bandwidth twice.

The Right Way: Presigned URLs

Browser → Your Server (get presigned URL)
Browser → S3 directly (upload)
Browser → Your Server (confirm upload complete)
Enter fullscreen mode Exit fullscreen mode

Your server only handles two tiny JSON requests. S3 handles the actual upload.

Server: Generate Presigned URL

import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';

const s3 = new S3Client({ region: process.env.AWS_REGION });

app.post('/api/upload/presigned', async (req, res) => {
  const { fileName, fileType, fileSize } = req.body;

  // Validate before issuing URL
  if (fileSize > 10 * 1024 * 1024) { // 10MB limit
    return res.status(400).json({ error: 'File too large' });
  }

  const key = `uploads/${req.user.id}/${Date.now()}-${fileName}`;

  const command = new PutObjectCommand({
    Bucket: process.env.S3_BUCKET,
    Key: key,
    ContentType: fileType,
    ContentLength: fileSize,
    Metadata: { userId: req.user.id },
  });

  const presignedUrl = await getSignedUrl(s3, command, { expiresIn: 300 }); // 5 min

  res.json({ presignedUrl, key });
});
Enter fullscreen mode Exit fullscreen mode

Client: Upload Directly to S3

async function uploadFile(file: File) {
  // Step 1: Get presigned URL from your server
  const { presignedUrl, key } = await fetch('/api/upload/presigned', {
    method: 'POST',
    body: JSON.stringify({ fileName: file.name, fileType: file.type, fileSize: file.size }),
  }).then(r => r.json());

  // Step 2: Upload directly to S3
  await fetch(presignedUrl, {
    method: 'PUT',
    body: file,
    headers: { 'Content-Type': file.type },
  });

  // Step 3: Confirm with your server
  await fetch('/api/upload/confirm', {
    method: 'POST',
    body: JSON.stringify({ key }),
  });

  return key;
}
Enter fullscreen mode Exit fullscreen mode

S3 CORS Configuration

[
  {
    "AllowedOrigins": ["https://yourapp.com"],
    "AllowedMethods": ["PUT", "POST"],
    "AllowedHeaders": ["Content-Type", "Content-Length"],
    "MaxAgeSeconds": 3000
  }
]
Enter fullscreen mode Exit fullscreen mode

Progress Tracking

const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', (e) => {
  if (e.lengthComputable) {
    const percent = (e.loaded / e.total) * 100;
    setProgress(Math.round(percent));
  }
});
xhr.open('PUT', presignedUrl);
xhr.setRequestHeader('Content-Type', file.type);
xhr.send(file);
Enter fullscreen mode Exit fullscreen mode

File upload with S3 presigned URLs, progress tracking, and file validation are built into the AI SaaS Starter Kit.

Top comments (0)