Integrating an Image Upload System into a React app with Node.js, Express.js and Multer
In this blog post, I’m going to walkthrough integrating an image upload system into a react app. I wanted to make this walkthrough because I had to build a similar sort of system in a MERN stack app I’m building and did not realize that uploading images from a local computer into a database to display on the frontend of our app would be quite as complicated as it ended up being, so hopefully those of you that want to build this same functionality will find this rudimentary demonstration helpful!
This very simple application built using Node.js, Express.js, and React.js will have three main features:
- A form for uploading images.
- A way to preview the image prior to uploading.
- A container that holds all the uploaded images.
First things first, I created a directory for the project. Make sure you have node and npm installed to build this project. You can ensure it with the following commands.
If they’re installed, cd into your project’s directory and run npm init and fill out the forms as needed/according to your personal preferences. This will create a package.json file where we will be storing our scripts and our dependencies. For this project, I installed 5 packages, 3 of which are absolutely necessary for our purposes. I’ve provided links to the documentation on each of the following dependencies.
- https://expressjs.com
- https://www.npmjs.com/package/concurrently
- https://www.npmjs.com/package/nodemon
- https://www.npmjs.com/package/multer
- https://www.npmjs.com/package/cors
Once installed, let’s create our server. First we create our express app, and we make sure to immediately invoke cors within our express app. Cors stands for Cross-Origin-Resource-Sharing and it allows us to retrieve data from another domain that is not the domain we are currently on. As you can see, I ran my app on port 3200. When we create our react app in a future step, a react app port defaults to port 3000, and since we are going to be retrieving images from our backend API (an outside domain — port 3200), we need to install and utilize cors, so that our app will accept requests from localhost:3000.
Nodemon is a great package to have installed as we can run our server and nodemon watches for updates in our code as we make them. In our package.json file, I created a script for running nodemon with the simple command npm run server. As you can see npm run start would start the node server, but running nom run server runs nodemon allowing us to see updates on our server in realtime.
When we run npm run server in the terminal, here’s what we see.
What we should see is nodemon start up along with a call to node server.js (server.js is the name of the file in which we have constructed the express app — the file through which we’ve been walking). We also should see the console log within our app.listen function on line 12 which also shows us on which port our server is currently running.
On line 8, we are creating our first route (in this case, with our current port, this route would be http://localhost:3200/). If our server is running and we make an http request to that URL from our browser, we should see the response JSON data on the window.
On line 10, we are setting up a root directory of static files using Express’s built-in middleware static function, which allows us to load and serve static files from our server. Within a directory called uploads, we will be storing our uploaded image files. On line 12 we are declaring our route from which our server can receive requests. As you can see, where importing a module from the path ‘routes/api/images’. I build my routes in that way as convention. First I create a directory called routes, within which I make a directory called api, and within api, I create all my files where I declare the routes according to perhaps models or collections or tables. In this case, we only care about uploading and displaying images, so I’ve created a file called images where we will utilize two actions: GET for retrieving all of the images and POST for creating the images.
In this example, we’re not using any database for storage. We are merely saving and retrieving files from a folder called uploads. In our first route, I’m using express’s built-in router method to make a GET action for our images. If we were to make a request at ‘http://localhost:3200/api/images') we should see JSON with a key files that holds an array of our image files! If the uploads folder is empty, we will receive a message ‘No Images Uploaded!’.
Now let’s configure multer. In the first quote on Multer via npmjs.com, “Multer is a node.js middleware for handling multipart/form-data , which is primarily used for uploading files.” Multer is an amazing package that allows us to monitor, place, and manage files as they come in to our backend. Back in our route file for images, let’s configure multer.
First we must import multer. Then we use multer’s built-in diskStorage function and pass in an object, within which we can do a variety of things from place our files as they are uploaded into a particular directory, rename the file, and even make sure the file is the right file type and/or is smaller than a particular size. In our case, we’re saving the folders to our uploads directory (make sure you in fact have a directory called uploads in the root of your app — multer, though magical, is not going to make one for you!) declaring the destination key equal to function that holds a callback, and that callback’s second argument is where we hold the directory name. In our filename key, we set our filename’s equal to the originalname key on the file object as it comes with a key original name, which holds the file name that the file had prior to being uploaded. After storing the multer method called with our storage object as it’s argument, as an object called uploads, we can now pass the uploads object as middleware in any of our routes.
Our post method for uploading images is very simple. In this method, we are expecting a single image so we call the single method from the uploads object, and we’ll be watching for the key ‘image’ which will have the value of the image. As part of our multipart form-data we’re receiving in the backend, the image will be saved to req.file, and the path of the image will be req.file.path. Our file, upon a post request, will be stored in the uploads directory, and we will send a JSON message from the server to indicate the upload’s success. Within this file, we may want to perform asynchronous actions such as storing data in a database, but for our purposes, we only need to store files in the uploads directory.
I recommend to test both our get and post routes out in postman for example and make sure they are working before we integrate our frontend. For the post route in Postman, we’ll want to make a post request to ‘http://localhost:3200/api/images', and give it a header of ‘Content-Type’: ‘multipart/form-data’. In the body, we will upload our file with the key image. If all goes well, we should see our success message in the Postman terminal!
THE FRONTEND
Onto the react app. *DISCLAIMER: I USED 0 CSS, SO I APOLOGIZE IN ADVANCE FOR THE VISUALS TO COME*. I started by creating a react app within our root directory called client, and deleted a lot of the things we won’t be needing for this particular project like service workers, the logo, and other files. Once the react app is set up, lets cd into our react app’s directory and install the axios package, which I prefer to use for data fetching and sending (though you can really use any method you prefer). After axios is installed, let’s set up concurrently from our root directory. Concurrently is an amazing package that allows us to run servers simultaneously with one command. In the package.json of the root, we want to create scripts that allow us to do just that using concurrently as such.
Firstly, I’ve added a script command called “client” if we run npm run client, it would start up our react app. The prefix flag is followed by the name of our react app’s directory. Now with the dev command, we use concurrently to run our server with nodemon and the react app at once, so ports 3200 and 3000 will be going simultaneously from the same terminal window!
Back to our React app. In the app component, I import the two components we’ll be using for this project.
1.) ImageContainer — image retrieval from our server and display
2.) ImageForm — form for uploading and previewing images to our server
Another important piece of logic I’ve added is what I’ll call a listener of sorts in our App component’s state for new image uploads aptly called newImage. Every time we submit a new image for upload, we call setNewImage and append a string that merely says ‘New Image’ to our newImage array’s state. We do this to notify our ImageContainer when to update and make a new GET request. I’ve done this hack method of updating state, as I’m not configuring redux or using React’s Context API for this project, and basically wanted a simple, quick (though clumsy) fix to showcase image uploading!
In ImageContainer we will be making a GET request to the server for image retrieval. If our files our array is truthy (as in, it contains images) we will store our images in state; otherwise, our response data will hold our message of ‘No Images Uploaded!’ as we configured in the backend, and we will set that in state. Our getImages function is called upon mounting and updates made, which perhaps is not the most efficient way to retrieving our images as we are retrieving them all on each re-render, but for our purposes, this will do for now. (For a more optimal configuration, we could create a route that retrieves all the images on component mounting, and then listens for the latest images uploaded and then to append that to our state as an update). In our useEffect, we use the newImage prop from our parent App component as a dependency, commanding that whenever, an update is made to our newImage prop, make a new get request to our backend API.
Within the return statement, we use the ternary operation to conditionally render images if they exist. If they do not, we will display our fallback; if they do, we’ll iterate through our images array and display each image. In our configure image method, we use our server URL “http://localhost:3200/“ and the filename as it is saved in our backend to access and display the image! I have decided to modularize our server’s URL declaration as API_URL because we are going to be using it in several components, so I wanted to keep our code more DRY.
If we run our react app, we should see the fallback as I, personally, have yet to upload any photos! Now time to build that form below in our ImageForm component.
Within our ImageForm component, we want to complete the logic for our other two goals aforementioned at the beginning of this post: preview images, and then actually submit uploads to our server.
In the return statement for our ImageForm component we conditionally render the form or a preview of our image if there is in fact an image uploaded. Our input type is file, and it accepts only png, jpg, or jpeg. When we upload a file, it calls our onChange prop’s method called handleImageUpload. In handleImageUpload we set our image state equal to e.target.files[0]. e.target.files holds a file list of all the files we’ve added in our form, and since we want the first, we only store the first index, which is an object that holds the necessary information. We also want to setPreview to true, which display a preview of the image we’ve selected. Our preview box has a button that clears the image and displays it. We display the image using the URL object’s method createObjectURL and calling it with image passed in as its argument. If we submit an image, by pressing Upload! It should clear the form, set preview to false, and should pass our image into the method uploadAction which I’ve abstracted into another file. Last but not least, we call handleNewImage, which is the prop we’ve passed from our app component that appends a new string to our newImage state.
Below is the uploadAction method, where we take our image and append it to form data which we pass in as our post request to axios as the body.
Let’s say I add an image called broccoli.jpg I mysteriously have saved on my desktop. Once uploaded I should see this:
Now that an image is in our ImageForm’s image state, we should see a preview of the image. In the top right corner, there is a button for clearing the preview, and in the bottom right, there is a button for uploading. If I click the upload button, here’s what we should see. We should see our form display again, but now, the broccoli is being displayed in our image container — even if we refresh the page!
And voila! A very simple demonstration of uploading and displaying images in a full stack fully JavaScript application using node.js, express.js, and react!!