coderain blog

How to Download Files from AWS S3 Bucket Client-Side with ReactJS & NodeJS: A Step-by-Step Guide for Webmerge Uploaded Files

In modern web applications, securely managing file storage and delivery is critical. AWS S3 (Simple Storage Service) is a popular choice for storing files due to its scalability, reliability, and security features. If you’re using tools like Webmerge (a document generation platform) to create files (e.g., PDFs, contracts, invoices) and storing them in S3, you may need to let users download these files directly from your ReactJS frontend.

However, directly exposing S3 credentials in client-side code is risky. Instead, we’ll use a secure approach: a Node.js backend to generate presigned URLs (temporary, time-limited access links) for S3 objects. The React frontend will request these URLs from the backend and trigger downloads—keeping credentials safe and control in your hands.

This guide walks you through every step, from setting up AWS S3 and Webmerge to building the React frontend and Node.js backend. By the end, you’ll have a fully functional system for client-side downloads of Webmerge-generated files stored in S3.

2026-01

Table of Contents#

  1. Prerequisites
  2. Step 1: Set Up Your AWS S3 Bucket
    • 1.1 Create an S3 Bucket
    • 1.2 Configure CORS for the Bucket
    • 1.3 Create an IAM User with S3 Permissions
  3. Step 2: Configure Webmerge to Upload Files to S3
  4. Step 3: Set Up the Node.js Backend
    • 3.1 Initialize the Node.js Project
    • 3.2 Install Dependencies
    • 3.3 Configure AWS SDK v3
    • 3.4 Create an Endpoint to Generate Presigned URLs
  5. Step 4: Build the ReactJS Frontend
    • 4.1 Create a React App
    • 4.2 Install Dependencies
    • 4.3 Build a Download Component
  6. Step 5: Test the Implementation
  7. Troubleshooting Common Issues
  8. Conclusion
  9. References

Prerequisites#

Before starting, ensure you have the following:

  • An AWS account (with permissions to create S3 buckets and IAM users).
  • A Webmerge account (sign up here if you don’t have one).
  • Basic knowledge of ReactJS (frontend) and Node.js/Express (backend).
  • Node.js (v14+) and npm/yarn installed.
  • A code editor (e.g., VS Code).

Step 1: Set Up Your AWS S3 Bucket#

1.1 Create an S3 Bucket#

First, create an S3 bucket to store Webmerge-generated files:

  1. Go to the AWS S3 Console.
  2. Click Create bucket.
  3. Enter a unique bucket name (e.g., my-webmerge-files-2024).
  4. Choose a region (e.g., us-east-1).
  5. Under "Object Ownership," select ACLs disabled (recommended for security).
  6. Leave other settings as default and click Create bucket.

1.2 Configure CORS for the Bucket#

CORS (Cross-Origin Resource Sharing) allows your React frontend (running on a different origin, e.g., http://localhost:3000) to access S3 resources.

  1. In your S3 bucket, go to the Permissions tab.
  2. Scroll to Cross-origin resource sharing (CORS) and click Edit.
  3. Paste the following CORS configuration (replace http://localhost:3000 with your frontend’s production URL later):
    [
      {
        "AllowedHeaders": ["*"],
        "AllowedMethods": ["GET"],
        "AllowedOrigins": ["http://localhost:3000"],
        "MaxAge": 3000
      }
    ]  
  4. Click Save changes.

1.3 Create an IAM User with S3 Permissions#

To allow your Node.js backend to access S3, create an IAM user with limited permissions:

  1. Go to the AWS IAM Console.
  2. Navigate to Users > Add users.
  3. Enter a username (e.g., s3-webmerge-downloader).
  4. Under "Select AWS access type," check Access key - Programmatic access.
  5. Click Next: Permissions.
  6. Select Attach existing policies directly > Create policy.
    • Use the JSON editor to paste this policy (replace your-bucket-name):
      {
        "Version": "2012-10-17",
        "Statement": [
          {
            "Effect": "Allow",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::your-bucket-name/*"
          }
        ]
      }  
    • Name the policy (e.g., S3WebmergeGetObjectPolicy) and click Create policy.
  7. Back in the "Add user" flow, search for your new policy and select it.
  8. Click Next > Create user.
  9. Save the Access key ID and Secret access key (you’ll need these for the backend).

Step 2: Configure Webmerge to Upload Files to S3#

Webmerge can automatically upload generated files to your S3 bucket. Here’s how to set it up:

  1. Log in to Webmerge.
  2. Go to Documents > Select your document (or create a new one).
  3. Click the Deliver tab > Add Delivery > Amazon S3.
  4. Configure the S3 integration:
    • Access Key ID: Use the IAM user’s access key from Step 1.3.
    • Secret Access Key: Use the IAM user’s secret key.
    • Bucket Name: Your S3 bucket name (e.g., my-webmerge-files-2024).
    • Path: Optional folder (e.g., webmerge-output/—files will be stored as s3://bucket-name/webmerge-output/filename.pdf).
    • File Name: Use Webmerge’s template variables (e.g., {{timestamp}}-invoice.pdf to generate unique filenames).
  5. Save the delivery settings.

Webmerge will now upload generated files to your S3 bucket at the specified path. Note the full S3 object key (e.g., webmerge-output/20240520-invoice.pdf)—you’ll need this to download files later.

Step 3: Set Up the Node.js Backend#

The backend will generate presigned URLs for S3 objects. Presigned URLs are temporary, secure links that grant limited access to download a file (without exposing AWS credentials to the frontend).

3.1 Initialize the Node.js Project#

  1. Create a new folder for your backend (e.g., s3-download-backend).
  2. Run npm init -y to initialize a package.json.

3.2 Install Dependencies#

Install required packages:

npm install express cors dotenv @aws-sdk/client-s3 @aws-sdk/s3-request-presigner  
  • express: Web framework for Node.js.
  • cors: Enable CORS for the backend.
  • dotenv: Load environment variables.
  • @aws-sdk/client-s3: AWS SDK v3 for S3 interactions.
  • @aws-sdk/s3-request-presigner: Generate presigned URLs.

3.3 Configure AWS SDK v3#

Create a .env file in your backend folder to store secrets:

PORT=5000  
AWS_REGION=us-east-1  
AWS_ACCESS_KEY_ID=your-iam-access-key  
AWS_SECRET_ACCESS_KEY=your-iam-secret-key  
S3_BUCKET_NAME=your-bucket-name  
S3_WEBMERGE_PATH=webmerge-output/  # Path where Webmerge stores files (from Step 2)  

3.4 Create an Endpoint to Generate Presigned URLs#

Create an index.js file (the backend entry point):

require('dotenv').config();  
const express = require('express');  
const cors = require('cors');  
const { S3Client, GetObjectCommand } = require('@aws-sdk/client-s3');  
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');  
 
const app = express();  
 
// Middleware  
app.use(cors()); // Allow CORS from all origins (restrict in production)  
app.use(express.json());  
 
// Initialize S3 Client  
const s3Client = new S3Client({  
  region: process.env.AWS_REGION,  
  credentials: {  
    accessKeyId: process.env.AWS_ACCESS_KEY_ID,  
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY  
  }  
});  
 
// Endpoint to generate presigned URL  
app.get('/get-presigned-url', async (req, res) => {  
  try {  
    const { fileName } = req.query; // Get filename from frontend  
 
    if (!fileName) {  
      return res.status(400).json({ error: 'fileName is required' });  
    }  
 
    // S3 object key = Webmerge path + filename (e.g., "webmerge-output/invoice.pdf")  
    const key = `${process.env.S3_WEBMERGE_PATH}${fileName}`;  
 
    // Create a GetObject command  
    const command = new GetObjectCommand({  
      Bucket: process.env.S3_BUCKET_NAME,  
      Key: key  
    });  
 
    // Generate presigned URL (expires in 5 minutes)  
    const presignedUrl = await getSignedUrl(s3Client, command, { expiresIn: 300 });  
 
    res.json({ presignedUrl });  
  } catch (error) {  
    console.error('Error generating presigned URL:', error);  
    res.status(500).json({ error: 'Failed to generate download URL' });  
  }  
});  
 
// Start server  
app.listen(process.env.PORT, () => {  
  console.log(`Backend running on http://localhost:${process.env.PORT}`);  
});  

Step 4: Build the ReactJS Frontend#

The React app will let users input a filename, request a presigned URL from the backend, and trigger a download.

4.1 Create a React App#

  1. Create a new React app:
    npx create-react-app s3-download-frontend  
    cd s3-download-frontend  

4.2 Install Dependencies#

Install axios to make API calls to the backend:

npm install axios  

4.3 Build a Download Component#

Replace src/App.js with this code:

import { useState } from 'react';  
import axios from 'axios';  
import './App.css';  
 
function App() {  
  const [fileName, setFileName] = useState('');  
  const [loading, setLoading] = useState(false);  
  const [error, setError] = useState('');  
 
  const handleDownload = async () => {  
    setLoading(true);  
    setError('');  
 
    try {  
      // Request presigned URL from backend  
      const response = await axios.get('http://localhost:5000/get-presigned-url', {  
        params: { fileName }  
      });  
 
      const { presignedUrl } = response.data;  
 
      // Trigger download by creating a temporary anchor tag  
      const anchor = document.createElement('a');  
      anchor.href = presignedUrl;  
      anchor.download = fileName; // Set the filename users see  
      document.body.appendChild(anchor);  
      anchor.click();  
      document.body.removeChild(anchor);  
 
    } catch (err) {  
      setError('Failed to download file. Check filename or try again later.');  
      console.error('Download error:', err);  
    } finally {  
      setLoading(false);  
    }  
  };  
 
  return (  
    <div className="App">  
      <h1>Download Webmerge Files from S3</h1>  
      <div className="download-container">  
        <input  
          type="text"  
          placeholder="Enter filename (e.g., invoice.pdf)"  
          value={fileName}  
          onChange={(e) => setFileName(e.target.value)}  
          disabled={loading}  
        />  
        <button onClick={handleDownload} disabled={loading || !fileName}>  
          {loading ? 'Generating Link...' : 'Download File'}  
        </button>  
      </div>  
      {error && <p className="error">{error}</p>}  
    </div>  
  );  
}  
 
export default App;  

Add basic styling in src/App.css:

.App {  
  text-align: center;  
  padding: 2rem;  
}  
 
.download-container {  
  margin: 2rem auto;  
  max-width: 500px;  
  display: flex;  
  gap: 1rem;  
}  
 
input {  
  flex: 1;  
  padding: 0.8rem;  
  font-size: 1rem;  
}  
 
button {  
  padding: 0.8rem 1.5rem;  
  background: #007bff;  
  color: white;  
  border: none;  
  border-radius: 4px;  
  cursor: pointer;  
}  
 
button:disabled {  
  background: #6c757d;  
  cursor: not-allowed;  
}  
 
.error {  
  color: #dc3545;  
  margin-top: 1rem;  
}  

Step 5: Test the Implementation#

  1. Start the Backend:
    In s3-download-backend, run node index.js. You should see:
    Backend running on http://localhost:5000.

  2. Start the Frontend:
    In s3-download-frontend, run npm start. The React app will open at http://localhost:3000.

  3. Generate a Test File with Webmerge:

    • In Webmerge, create a test document and trigger a merge. Webmerge will upload the file to your S3 bucket at s3://your-bucket-name/webmerge-output/filename.pdf.
  4. Download the File:

    • In the React app, enter the filename (e.g., invoice.pdf) and click "Download File".
    • The file should download to your device!

Troubleshooting Common Issues#

  • CORS Errors:

    • Ensure S3 CORS allows your frontend origin (check Step 1.2).
    • In the backend, restrict cors() to your frontend URL in production (e.g., cors({ origin: 'https://your-frontend.com' })).
  • "Access Denied" in Presigned URL:

    • Verify the IAM user has s3:GetObject permissions (Step 1.3).
    • Ensure the S3_WEBMERGE_PATH in .env matches the path Webmerge uses (e.g., webmerge-output/).
  • File Not Found:

    • Confirm the filename exists in S3 at s3://bucket-name/path/filename.
    • Check the S3 bucket in the AWS Console to verify the file was uploaded by Webmerge.
  • Presigned URL Expired:

    • The URL expires after 5 minutes (configured via expiresIn: 300 in the backend). Adjust this value if needed.

Conclusion#

You’ve successfully built a secure system to download Webmerge-generated files from AWS S3 using ReactJS and Node.js. By using presigned URLs, you avoid exposing AWS credentials to the frontend, ensuring security.

Next steps:

  • Add validation for filenames in the frontend.
  • List all available files in S3 (extend the backend to fetch object keys).
  • Add authentication to restrict downloads to authorized users.

References#