Upload Resized Images to Node.JS API with React

Overview

This article is a how-to for resizing files on the client side that are uploaded to a folder with the meta data stored in a SQLite database table. This example consists of a react client application for the client-side with a dropzone and a NodeJS API to handle the uploads. Also the client app will have image zoom so you can see the image quality after the resize. The resizer library is pretty easy to use when it comes to customizing the max image size and converting them to one of the available formats (JPEGPNG or WEBP). In the example all images will be converted to JPEG.

List of resolutions and the corresponding sizes.

SizeResolutionUse Case
8 kB8 kBLogo, Icon, or Avatar
30 kB640 x 480 pixels JPEGWeb page image
40 – 100 kB
500 kB1280×960 JPEGTypical smartphone image
1 MB2048×1536 (4 megapixel) JPEG Smartphone or digital camera

Download Source from GitHub https://github.com/fullstacksoup

GitHub

Part I – React Client Side App

Prerequisites

You will need to create and install the following libraries for this example. I recommend getting the source from GitHub unless you want to try it step by step.

Create a New React-App

npx create react-app multi-step-form-demo

Material UIVersion 4

NOTE: React-Material Version 4 was used due to Material-Drop-Zone not supporting MUI version 5.at the time this article was written.

For version 4 documentation click here
For the current version documentation click here
All documentation for all versions here

npm install @material-ui/core@4.12.3 @material-ui/icons@4.11.2 
npm install @material-ui/lab@4.0.0-alpha.60 
npm install @material-ui/pickers@3.3.10

Axios

Axios is a promise-based HTTP Client.

npm i axios

Material Dropzone

material-dropzone is used for uploading with thumbnail previews of multiple files.

npm i material-ui-dropzone

React File Resizer – (Optional)

react-image-file-resizer is a popular JavaScript library for resizing files. It is intuitive and has no other dependencies.

npm i react-image-file-resizer

Image List with Zoom Component

The “.\components\add-image-layout\ImageListWithZoom.jscomponent will display the images in a scroll enabled div. The user can click on the image and zoom in and click back to the smaller size.

import React, { useEffect, useState } from 'react';
import { makeStyles } from '@material-ui/core/styles';
import ImageList from '@material-ui/core/ImageList';
import axios from "axios";
import Zoom from 'react-medium-image-zoom'
import 'react-medium-image-zoom/dist/styles.css'

const useStyles = makeStyles((theme) => ({
  root: {
    display: 'flex',
    flexWrap: 'wrap',
    justifyContent: 'space-around',
    overflow: 'hidden',
    backgroundColor: theme.palette.background.paper,
  },
  imageList: {
    width: 600,
    height: 450,
  },
}));


export default function ImageListWithZoom(props) {
  const classes = useStyles();
  const [imageArr, setImageArr] = useState([]);
  
  useEffect(() => {
    const apiurlGet = `http://localhost:44100/api/images`;        
    axios({
        method: 'get',
        url: apiurlGet,
        headers: {'Content-Type': 'application/json' }
    })
    .then(function (response) {
        // handle success
        // add a non-binary file    
        setImageArr(response.data.data)
        console.log('useEffect ', response.data.data);
    })
    .catch(function (response) {
        //handle error
        console.log(response);
    });
  }, [props.imgData]);

  
  return (
    <div className={classes.root}>
      <ImageList rowHeight={160} className={classes.imageList} cols={4}>
        {imageArr.map((item) => (
          <Zoom>
          <picture>
            <source media="(max-width: 800px)" srcSet={`http://localhost:44100/uploads/${item.Filename}`} />
            <img
              alt="that wanaka tree"
              src={`http://localhost:44100/uploads/${item.Filename}`}
              width="190"
            />
          </picture>
        </Zoom>            
        ))}
      </ImageList>
    </div>
  );
}

Dropzone with File Resizer

The Material DropZone will take up to 3 files with a size limit of 10mb. As they are loaded from the DropZone the images will get resized if the exceed the 600×800 size threshold.

Imports and state variables used

import React, { useState } from "react";
import Resizer from "react-image-file-resizer";
import { DropzoneArea } from 'material-ui-dropzone';
import AttachFile from '@material-ui/icons/PhotoCamera';
import { Button } from "@material-ui/core";
import axios from "axios";


export default function AddImageLayout() {
    const [newImage, setNewImage] =  useState('');
    const [files, setFiles] =  useState('');

With the react-image-file-resizer compressFormat property can be either JPEGPNG or WEBP. In this example the compression type is JPEG. Below are the properties from the React Image File Resizer documentation which can be viewed here.

OptionDescriptionTypeRequired
filePath of image fileobjectYes
maxWidthNew image max width (ratio is preserved)numberYes
maxHeightNew image max height (ratio is preserved)numberYes
compressFormatCan be either JPEGPNG or WEBP.stringYes
qualityA number between 0 and 100. Used for the JPEG compression.(if no compress is needed, just set it to 100)numberYes
rotationDegree of clockwise rotation to apply to the image. Rotation is limited to multiples of 90 degrees.(if no rotation is needed, just set it to 0) (0, 90, 180, 270, 360)numberYes
responseUriFuncCallback function of URI. Returns URI of resized image’s base64 format. ex: uri => {console.log(uri)});functionYes
outputTypeCan be either base64blob or file.(Default type is base64)stringNo
minWidthNew image min width (ratio is preserved, defaults to null)numberNo
minHeightNew image min height (ratio is preserved, defaults to null)numberNo

Here is the resizer set to have a max size of 600 x 800 which is about 50kb in size.

    const resizeFile = (file) =>
        new Promise((resolve) => {
        Resizer.imageFileResizer(
            file,
            600,
            800,
            "JPEG",
            80,
            0,
            
            (image) => {
            Object.assign(image, {
                preview: URL.createObjectURL(image),
                path: image.name,
                
            });
            resolve(image);
            },
            "file"
        );
    });
    

Handling the Dropzone. As images are dragged on or picked they will be resized before added to the files array.

  const handleDropzoneChange = async (images) => {
        console.log('handleDropzoneChange  ', images);     
        await Promise.all(
            images.map((image) => {
              return resizeFile(image);
            })
          ).then((uploadBranchImages) => {            
            console.log(uploadBranchImages);
            setFiles(uploadBranchImages);       
          });

        setTimeout(() => {
            console.log('handleDropzoneChange  files ', files);     
        }, 1500);

    }

Alert Dialog Component – (Optional)

The “.\components\add-image-layout\AlertDialog.js” is a popup dialog with simple message to let the user know the files have been uploaded. A good alternative would be React Sweet Alert 2 or just a simple JavaScript alert().

import React from 'react';
import Button from '@material-ui/core/Button';
import Dialog from '@material-ui/core/Dialog';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import DialogContentText from '@material-ui/core/DialogContentText';
import DialogTitle from '@material-ui/core/DialogTitle';

export default function AlertDialog(props) {
  const [open, setOpen] = React.useState(props.show);


  const handleClose = () => {
    props.closeDialogBox();
  };

  return (
    <div>

      <Dialog
        open={props.show}
        onClose={handleClose}
        aria-labelledby="alert-dialog-title"
        aria-describedby="alert-dialog-description"
      >
        <DialogTitle id="alert-dialog-title">{"Success"}</DialogTitle>
        <DialogContent>
          <DialogContentText id="alert-dialog-description">
            Image file(s) have been uploaded
          </DialogContentText>
        </DialogContent>
        <DialogActions>
          <Button onClick={handleClose} color="primary" autoFocus>
            Close
          </Button>
        </DialogActions>
      </Dialog>
    </div>
  );
}

Parent Page Component – (Dropzone)

The “.\pages\add-image-layout\index.js” is the parent component for the dropzone and the images list. This calls the API to upload the images and then the get images that triggers the ImageListWithZoom.js component to load the images. the useEffect() hook is used to load images if there are any when starting the page.

import React, { useEffect, useState } from "react";
import Resizer from "react-image-file-resizer";
import { DropzoneArea } from 'material-ui-dropzone';
import styles from 'styles/UploadFileForm.module.css';
import AttachFile from '@material-ui/icons/PhotoCamera';
import { Button } from "@material-ui/core";
import axios from "axios";
import ImageListWithZoom from "components/add-image-layout/ImageListWithZoom";
import AlertDialog from "components/add-image-layout/AlertDialog"; // Optional Alert Dialog 
export default function AddImageLayout() {
    const [newImage, ] =  useState('');
    const [files, setFiles] =  useState('');    
    const [imgData, setImgData] =  useState([]);
    const [showMessage, setShowMessage] =  useState(false);
    
    const resizeFile = (file) =>
        new Promise((resolve) => {
        Resizer.imageFileResizer(
            file,
            600,
            800,
            "JPEG",
            80,
            0,
            
            (image) => {
            Object.assign(image, {
                preview: URL.createObjectURL(image),
                path: image.name,
                
            });
            resolve(image);
            },
            "file"
        );
    });

    const closeDialogBox = () => {        
        setShowMessage(false);
    }
   
    const handleFormSubmit = () => {        
        const formData = new FormData()
        // add a non-binary file    
        formData.append('UserId', 2);

        for (let i = 0; i < files.length; i++) {
            formData.append(`files[${i}]`, files[i])
        }
        
        const apiurl = `http://localhost:44100/api/upload-multiple-files`;
        axios({
            method: 'post',
            url: apiurl,
            data: formData,
            headers: {'Content-Type': 'multipart/form-data' }
            })
            .then(function (response) {
                //handle success
                console.log(response);
                
                const apiurlGet = `http://localhost:44100/api/images`;        
                axios({
                    method: 'get',
                    url: apiurlGet,
                    headers: {'Content-Type': 'application/json' }
                    })
                    .then(function (response) {
                        //handle success
                        // add a non-binary file    
                        setImgData(response.data)
                        console.log(response);
                    })
                    .catch(function (response) {
                        //handle error
                        console.log(response);
                    });
                    
                    
            })
            .catch(function (response) {
                //handle error
                console.log(response);
            });
            setShowMessage(true);
        // alert(JSON.stringify(data, null, 2));
    }

    const handleDropzoneChange = async (images) => {
        console.log('handleDropzoneChange  ', images);     
        await Promise.all(
            images.map((image) => {
              return resizeFile(image);
            })
          ).then((uploadBranchImages) => {            
            console.log(uploadBranchImages);
            setFiles(uploadBranchImages);       
          });

        setTimeout(() => {
            console.log('handleDropzoneChange  files ', files);     
        }, 1500);

    }

    useEffect(() => {
        const apiurlGet = `http://localhost:44100/api/images`;        
        axios({
            method: 'get',
            url: apiurlGet,
            headers: {'Content-Type': 'application/json' }
            })
            .then(function (response) {
                //handle success
                // add a non-binary file    
                setImgData(response.data)
                console.log(response);
            })
            .catch(function (response) {
                //handle error
                console.log(response);
            });

    }, [])


    return (
        <div className="App">
        
        <img src={newImage} alt="" />


        <DropzoneArea   filesLimit={3}                         
                        maxFileSize={10000000}
                        previewText="Selected files"
                        onChange={handleDropzoneChange }                                
                        initialFiles={files}                        
                        Icon={AttachFile}                                
                        acceptedFiles={['image/*']}
                        showAlerts={['error']}                                 
                        dropzoneText="Click here to upload photo"
                       />

        <Button type="submit" variant="outlined" onClick={handleFormSubmit} >Upload</Button>        

        <ImageListWithZoom imgData={imgData}/>

        <AlertDialog show={showMessage} closeDialogBox={closeDialogBox}/>

        </div>        
    );
}

Part II – NodeJS

Prerequisites

If you don’t already have Node.js installed then you can get the download and install information from their website at https://nodejs.org/.

Install Express

Create a folder for your project and go into that directory and run the following commands.

npm init
npm install express

Install Nodemon

Nodemon restarts the node.js application when changes are made.

npm install nodemon

Update the “scripts” section of the “package.json” file with the following:

  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "nodemon  app.js"
  },

To start the application use npm start instead of node app.js. This enables hot reloads so you don’t have to restart your API every time it is updated.

Install the SQL Lite Driver.

npm install sqlite3

Handling CORS

Cross Origin Resource Sharing is needed if you want to use the API with an external application.

npm i cors

Part III – The API

The way to loop through file when the JSON data has the label in quotes like the following:

“./app.js” Source Code

  'files[0]': {
    name: 'pexels-dids-8175462.JPEG',
    data: <Buffer ff d8 ff e0 00 10 4a 46 49 46 00 01 01 00 00 01 00 01 00 00 ff e2 02 28 49 43 43 5f 50 52 4f 46 49 4c 45 00 01 01 00 00 02 18 00 00 00 00 02 10 00 00 ... 15308 more bytes>,
    size: 15358,
    encoding: '7bit',
    tempFilePath: '',
    truncated: false,
    mimetype: 'image/jpeg',
    md5: 'dec92b4b1d557d94d0caf520b782d93a',
    mv: [Function: mv]
  },
  'files[1]': {
    name: 'pexels-hugo-magalhaes-9697600.JPEG',
    data: <Buffer ff d8 ff e0 00 10 4a 46 49 46 00 01 01 00 00 01 00 01 00 00 ff e2 02 28 49 43 43 5f 50 52 4f 46 49 4c 45 00 01 01 00 00 02 18 00 00 00 00 02 10 00 00 ... 11484 more bytes>,
    size: 11534,
    encoding: '7bit',
    tempFilePath: '',
    truncated: false,
    mimetype: 'image/jpeg',
    md5: '7f9669d4a2fb4523ffb94c3bf747f0d9',
    mv: [Function: mv]
  }
}

Using the Object.keys() function to make the data easier to work with.

req.files[Object.keys(req.files)[0]];

Now we can loop through the files with a .forEach() property.

Object.keys(req.files).forEach(element => {
   //...
})

Imports and Database Setup

With the useEffect() hook set to run when a change is made to another state variable.

const express = require('express');
const app = express();
const port = 44100;
var fs = require('fs');
const cors = require('cors');
const multer  = require('multer')
const { v4: uuidv4 } = require('uuid');
const fileupload = require("express-fileupload");
var sqlite3 = require("sqlite3"),
    TransactionDatabase = require("sqlite3-transactions").TransactionDatabase;

const DBSOURCE = "usersdb.sqlite";

var db = new TransactionDatabase(
    new sqlite3.Database(DBSOURCE, (err) => {
    if (err) {
      // Cannot open database
      console.error(err.message)
      throw err
    } 
    else {
             
        db.run(`CREATE TABLE ProductImages (
            Id INTEGER PRIMARY KEY AUTOINCREMENT,
            Mimetype TEXT,                         
            Filename TEXT,                         
            Size INTEGER,                         
            DateCreated DATE
            )`,
        (err) => {
            if (err) {
                // Table already created
            }
        });  
    }
  })
);

module.exports = db

app.use(
    express.urlencoded(),
    cors(),
    fileupload(),
    express.static("files")    
);

Get Images Action

Get Action for a getting all the files.

app.get("/api/images", async (req, res, next) => {
    var sql = "SELECT * FROM ProductImages"
    var params = []
    db.all(sql, params, (err, rows) => {
        if (err) {
          res.status(400).json({"error":err.message});
          return;
        }
        res.json({
            "message":"success",
            "data":rows
        })
      });
});

How to Get a Single Image

Get Action rendering a single image. Simply use the express.static() method to define the resource path for the images.

Example: http://localhost:44100/uploads/ff5b402f-5901-4aa8-b99f-0f88619fbd0d.jpg

app.use('/uploads', express.static('uploads'));

Multiple File Upload Function

Post Action for a single file from React Dropzone


app.post("/api/upload-multiple-files", (req, res) => {    
    var dir = `./uploads/`;    
    if (!fs.existsSync(dir)){
        fs.mkdirSync(dir, { recursive: true });
    }

    var fileCount = 0;
    var files = [];
    var fileKeys = Object.keys(req.files);
    
    fileKeys.forEach(function(key) {
        files.push(req.files[key]);
    });
    
    files.forEach(element => {                
        var newFileName = `${uuidv4()}.jpg`;        
        var newPath = `./uploads/${newFileName}`;        
        fileCount++;
        
        
        var imageBinary = element.data;

        try {
            fs.writeFile(newPath, imageBinary, 'base64', function(err){});                
        } catch (error) {
            console.log(error);
        }                

        var data = {            
            Filename: newFileName,
            Mimetype: element.mimetype,
            Size: element.size,
            DateCreated: Date('now')
        }

        var sql ='INSERT INTO ProductImages (Filename, Mimetype, Size, DateCreated) VALUES (?,?,?,?)'
        var params = [data.Filename, data.Mimetype, data.Size, Date('now')]

        db.run(sql, params, function (err, result) {
            if (err){
                res.status(400).json({"error": err.message})
                return;
            }
        });       
    });

    res.json({
        message: `Successfully uploaded files`
    })    
    
});


Part IV – Running the Example.

Run npm start for the NodeJS API and the React App by each in a new instance in VS Code or an IDE of your choice.

Once both of them are running you should see a drop zone.

Drag or Click inside the dropzone and upload the Images under the “.\Large-Images” folder.

Click Upload and you should see something like the following:

Inside the API you will see a folder labeled .\uploads.

The files went from 1 – 5mb to no bigger than 56kb without losing its original orientation. 800×600 is low quality but the max height and width can be modified to the ideal size range for you.

Conclusion

Now that you can resize images and convert them to JPEG, try the different settings available from react-image-file-resizer to get the optimal size and resolution. This feature is extremely convenient for users since they won’t have to resize or reformat their images when uploading.