Oct 15, 2023 · 15 mins read
Using Signed URLs in React | File Upload using AWS S3, Node.js and React - Part 3
Building the react application to upload files directly to AWS S3 using Signed URLs generated from our node.js application.Umakant Vashishtha
Secure File Upload using AWS S3, Node.js and React using Signed URLs
In this three parts series, we are learning how to add secure file upload feature in your application using AWS S3, Node.js and React with Signed URLs.
Check out the previous parts if you haven’t
Part 1 | Setting Up S3 Bucket Part 2 | Node.js Server SetupTable of Contents
- Understanding the flow
- Setting up React App
- Testing the Signed URL in a Playground Component
- Reusable Components
In this part, we will build the react application to upload files directly to AWS S3 using Signed URLs generated from our node.js application.
If you prefer video tutorials, here is the series on YouTube.
Understanding the flow
Fig: File Upload Complete Flow
The above diagram shows the complete flow of the file upload process.
- User selects the file to upload
- React application sends a request to the node.js server to generate a signed URL
- Node.js server generates a signed URL by calling AWS S3 APIs with AWS Credentials and sends it back to the react application.
- React application uses the signed URL to upload the file directly to AWS S3.
Once the file is uploaded, we can save the URL in the database and use it to display the file in the application.
Setting up React App
Let’s start by creating a new react application using create-react-app
command.
Terminal npx create-react-app client
Once the application is created, we need to install the following dependencies.
Terminal npm install axios @emotion/react @emotion/styled @mui/icons-material @mui/material
We will use axios
to make API calls to our node.js server, @emotion
and @mui
for creating the styled components.
Setting up App Config
We will create a new file config.js
in the src
folder to store the configuration for our application.
src/config/index.jsconst config = {
API_BASE_URL: process.env.REACT_APP_API_BASE_URL,
};
export default config;
We will use REACT_APP_API_BASE_URL
environment variable to store the base URL of our node.js server.
Create a new file .env.development.local
in the root of the project and add the following content.
TerminalREACT_APP_API_BASE_URL=http://localhost:3010/api
Setting Up API Client
We will create a new file api/index.js
in the src
folder to create an API client using axios
to make API calls to our node.js server and to AWS S3.
src/api/index.jsimport axios from "axios";
import config from "../config";
const apiClient = axios.create({
baseURL: config.API_BASE_URL,
});
export async function getSignedUrl({ key, content_type }) {
const response = await apiClient.post("/s3/signed_url", {
key,
content_type,
});
return response.data;
}
export async function uploadFileToSignedUrl(
signedUrl,
file,
contentType,
onProgress,
onComplete
) {
axios
.put(signedUrl, file, {
onUploadProgress: onProgress,
headers: {
"Content-Type": contentType,
},
})
.then((response) => {
onComplete(response);
})
.catch((err) => {
console.error(err.response);
});
}
Testing the Signed URL in a Playground Component
We will create a new component Playground
in the src/components
folder to test the signed URL flow.
src/components/Playground.jsimport React, { useState } from "react";
import { getSignedUrl, uploadFileToSignedUrl } from "../api";
const Playground = () => {
const [fileLink, setFileLink] = useState("");
const onFileSelect = (e) => {
const file = e.target.files[0];
const content_type = file.type;
const key = `test/image/${file.name}`;
getSignedUrl({ key, content_type }).then((response) => {
console.log(response);
uploadFileToSignedUrl(
response.data.signedUrl,
file,
content_type,
null,
() => {
setFileLink(response.data.fileLink);
}
);
});
};
return (
<div>
<h1>Playground</h1>
<img src={fileLink} />
<input type="file" accept="*" onChange={onFileSelect} />
</div>
);
};
export default Playground;
We will add the Playground
component to the App.js
file to test the flow.
src/App.jsimport React from "react";
import Container from "@mui/material/Container";
import Playground from "./components/Playground";
function App() {
return (
<div className="App">
<Container style={{ display: "flex", justifyContent: "center" }}>
<Playground />
</Container>
</div>
);
}
Reusable Components
We can build reusable hook component from this code as below:
src/hooks/useFileUpload.jsimport { useCallback, useState } from "react";
import { getSignedUrl, uploadFileToSignedUrl } from "../api";
function getKeyAndContentType(file, prefix = "documents") {
const [fileName, extension] = file.name.split(".");
// to generate unique key everytime
let key = prefix + `/${fileName}-${new Date().valueOf()}.${extension}`;
let content_type = file.type;
return { key, content_type };
}
export default function useFileUpload(onSuccess, prefix) {
const [uploading, setUploading] = useState(false);
const [uploadProgress, setUploadProgress] = useState(null);
const uploadFile = useCallback((file) => {
if (file) {
const { key, content_type } = getKeyAndContentType(file, prefix);
getSignedUrl({ key, content_type }).then((response) => {
const signedUrl = response.data?.signedUrl;
const fileLink = response.data?.fileLink;
if (signedUrl) {
setUploading(true);
uploadFileToSignedUrl(
signedUrl,
file,
content_type,
(progress) => {
setUploadProgress((progress.loaded / progress.total) * 100);
},
() => {
onSuccess(fileLink);
setUploading(false);
}
).finally(() => {
setUploadProgress(0);
});
}
});
}
// eslint-disable-next-line
}, []);
return {
uploading,
uploadProgress,
uploadFile,
};
}
The usage for this hook would look something like this:
src/components/EditAvatar.jsimport React, { useEffect, useState } from "react";
import { MenuItem, Menu } from "@mui/material";
import useFileUpload from "../hooks/useFileUplaod";
function EditAvatar({ inputId, image, name, onChange, prefix = "avatars" }) {
const { uploadFile } = useFileUpload(onChange, prefix);
const [file, setFile] = useState(null);
useEffect(() => {
uploadFile(file);
// Do NOT put uploadFile function as dependency here
// eslint-disable-next-line
}, [file]);
return (
<div>
<img src={image} alt={name} className="edit-avatar" />
<input
type="file"
accept="image/jpeg, image/png"
onChange={(e) => {
setFile(e.target.files[0]);
}}
id={inputId}
className="edit-file-input"
/>
<div className="edit-menu-button">
<Menu>
<label htmlFor={inputId}>
<MenuItem>Upload New</MenuItem>
</label>
{image && (
<a href={image} target="_blank" rel="noreferrer">
<MenuItem>Preview</MenuItem>
</a>
)}
<MenuItem onClick={() => onChange(null)}>Remove</MenuItem>
</Menu>
</div>
</div>
);
}
You can find the rest of the code here:
https://github.com/umakantv/yt-channel-content/tree/main/file-upload-using-s3
Thank you for reading, please subscribe if you liked the content, I will share more such in-depth content related to full-stack development.
Happy learning. :)
Similar Articles
Setting Up Node.js App | File Upload using AWS S3, Node.js and React - Part 2
Setting up Node.js application and use AWS SDK to generate S3 Signed URL using AWS access credentials that will be used in our react application to upload files directly.
Oct 07, 2023 · 20 mins
Setting Up S3 Bucket | File Upload using AWS S3, Node.js and React - Part 1
Setting up S3 bucket and allowing public read access on a prefix URL
Oct 06, 2023 · 20 mins
Clustering - Run Multiple Instances of Node.js Application
Improving Node.js Application Performance With Clustering
May 05, 2023 · 25 mins