DEV Community

Cover image for The Smart Way to Achieve Fast and Secure File Uploads to S3 (Without a Server) 💥
Ashraf Amer
Ashraf Amer

Posted on

The Smart Way to Achieve Fast and Secure File Uploads to S3 (Without a Server) 💥

Uploading files is one of the most common operations in web and mobile applications, from profile pictures, invoices, and videos to uploading experimental files during pre-production or testing phases.

Traditionally, files are uploaded directly to the backend server, which then forwards them to a storage service like Amazon S3. While this approach seems simple, it introduces serious performance bottlenecks and potential security risks.

Every byte passing through your server adds load, latency, and liability

And here lies the real challenge: how can we upload large files without degrading performance or overloading the backend, and at the same time, without exposing our AWS credentials to the frontend, which would completely break security?

I often ask this question during interviews, and many of my colleagues have faced it too. It’s not a trivial problem, it impacts the entire application, from something as small as changing a profile picture to something as massive as uploading multi-gigabyte data batches for pre-production testing.

In this article, we’ll solve this puzzle in a practical, hands-on way, exploring how Presigned URLs make secure, fast, and serverless file uploads possible, with real-world examples and lessons learned from production systems.

🧠 Introducing Presigned URLs

When every file passes through the backend, your server must handle large file transfers, maintain open connections, and scale horizontally just to keep up. For growing platforms or high-traffic apps, this results in increased costs, longer response times, and sometimes downtime under heavy load.

Imagine thousands of users uploading high-resolution images simultaneously! your backend becomes a traffic jam.

Presigned URLs are your secure shortcut to S3 — direct, controlled, and temporary

To solve this, AWS introduced Presigned URLs, a simple yet powerful feature that allows clients to upload files directly to S3 without passing through your backend.
A Presigned URL is a time-limited link generated by your server that gives temporary permission to upload (or download) a file to a specific S3 bucket.

🧩 How It Works [Behind the Scenes]

No more talk. Let’s get practical. Below we’ll walk through the implementation step-by-step with concrete backend and client examples. The flow is simple:

1. Client requests permission to upload.

At this step the client should tell the backend what it intends to upload: file name, MIME type, and size. So the server can validate the request before issuing any permissions.

// 1) Validate request: filename, contentType, size
    const { filename, contentType, size } = req.body;
    if (!filename || !contentType || !size) return res.status(400).json({ error: 'Missing params' });

// [optional] enforce max file size and allowed types
    const MAX_SIZE = 5 * 1024 * 1024 * 1024; // 5 GB
    const allowed = ['image/png','image/jpeg','application/pdf','video/mp4'];
    if (!allowed.includes(contentType)) return res.status(400).json({ error: 'Invalid content type' });
    if (size > MAX_SIZE) return res.status(400).json({ error: 'File too large' });// 2) Create a safe key (avoid client-provided full paths)
    const key = `uploads/${uuidv4()}-${filename}`;

    // 3) Prepare PutObjectCommand
    const command = new PutObjectCommand({
      Bucket: process.env.S3_BUCKET,
      Key: key,
      ContentType: contentType,
      // You can set ACL: 'private' (default). Avoid public-read unless intended.
    });

    // 4) Generate presigned url (short expiry)
    const expiresIn = 60 * 5; // 5 minutes
    const presignedUrl = await getSignedUrl(s3, command, { expiresIn });

    // 5) Save upload record in DB (key, expected size, user id, status='pending')
    // await db.saveUpload({...})

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

2. Backend generates a presigned URL.

The server creates a safe S3 object key (e.g., using a UUID prefix), prepares a presigned URL with the specific permissions required (PUT), and returns that URL to the client. This preserves the backend’s role in validation and access control while avoiding direct handling of the file payload.

{
  "url": "https://your-bucket-name.s3.us-east-1.amazonaws.com/uploads/uuid-filename.png",
  "key": "uploads/uuid-filename.png",
  "expiresIn": 300
}
Enter fullscreen mode Exit fullscreen mode

3. Client uploads directly to S3 using the presigned URL.

The client sends the file content (typically with an HTTP PUT) to the presigned URL. The upload happens directly between the client and S3, so your backend never receives the file bytes, your AWS credentials remain private, and the server is not burdened by file transfer load, regardless of file size.

curl --location --request PUT 'https://your-bucket-name.s3.us-east-1.amazonaws.com/uploads/uuid-filename.png' \
--header 'Content-Type: image/png' \
--data-binary '@/path/to/local-file.png'
Enter fullscreen mode Exit fullscreen mode

Once you receive a 200 OK response (even with an empty body), it means the file has been successfully uploaded.
At this point, the client should notify the backend, sending back the file key, so the server can update the upload status or store the reference in the database.

This architecture shifts the heavy lifting from your servers to Amazon’s globally distributed infrastructure, improving both scalability and reliability.

🧾 Security & scope

One of the best aspects of presigned URLs is that they are time-bound and scoped. You can control exactly:

  • Which object key may be uploaded.
  • Which HTTP method is allowed (PUT).
  • How long the URL remains valid.

Security isn’t about hiding data — it’s about limiting trust by design

Even if a presigned URL is intercepted, it will expire and be useless after the configured time window, keeping your system secure.

🔐 Security Best Practices:

  • Never send AWS credentials to client.
  • Keep presigned URL expiration short (5–15 minutes).
  • Generate keys server-side (use uuid + directory).
  • Validate file metadata before issuing URL.
  • Store upload records for audits and to detect incomplete/abandoned uploads.
  • If needed، use server-side Lambda to scan files (virus/malware) after upload.
  • Put lifecycle rules on bucket for cleanup of pending or tmp-* uploads.

And just like that, we’ve achieved the perfect balance, in the simplest possible way.
We’ve kept the backend lightweight and free from upload load,
enabled direct file uploads to S3 regardless of file size or type,
and ensured that no credentials are ever exposed to the client.

The result? A secure, efficient, and blazingly fast upload process.

Top comments (0)