node.jsreactjsimagemultersrc

React: Image uploaded from node js backend is not rendering


I have used multer on my Node Js backend to upload files from my React frontend. I have been storing the files in the React public folder. I have been saving the image path in my MonogoDB database. The idea was use the image path to insert the image into my React frontend. The Account page makes a GET request to the backend retrieves the path however I can't get the image to display. The path when inspected with Dev tools 'http://localhost:3000/user/account/frontend/public/uploads/1621968408663.jpg'. The path sent from the GET request is '../frontend/public/uploads/1621968408663.jpg'. Am I going about this right way? What is the solution.

AccountPage.js

import React, {useEffect, useState} from "react";
import { Link, useParams } from "react-router-dom";
import Header from "./Header";
import axios from "axios";

import SettingsIcon from "@material-ui/icons/Settings";
import IconButton from "@material-ui/core/IconButton";

export default function AccountPage() {
    // Declare a new state variable, which we'll call "count"


    const { id } = useParams();

    const api = `http://localhost:5000/user/account/${id}`;

    const [ firstName, setFirstName ] = useState("");
    const [ lastName, setLastName ] = useState("");
    const [ emailAddress, setEmailAddress ] = useState("");
    const [ gender, setGender ] = useState("");
    const [ sexualPreference, setSexualPreference ] = useState("");
    const [ age, setAge ] = useState("");
    const [ description, setDescription ] = useState("");
    const [ matches, setMatches ] = useState([{}]);
    const [file, setFiles] = useState("")

    useEffect(() => {
      axios.get(api, {
        headers: {
          Authorization: localStorage.getItem("jwt"),
          "Content-Type": "application/json",
          "Cache-Control": "no-cache",
        },
      })
      .then((res) => {
        setFirstName(res.data.user.firstName)
        setLastName(res.data.user.lastName)
        setEmailAddress(res.data.user.emailAddress)
        setGender(res.data.user.gender)
        setSexualPreference(res.data.user.sexualPreference)
        setAge(res.data.user.age)
        setDescription(res.data.user.description)
        setMatches(res.data.user.matches)
        setFiles(res.data.user.path)
      });
    }, []);
        
    console.log(file)

    return (
      <div>
        <Header />
        <div>
        <img src={file}/>
        <p>{firstName} {lastName}</p>
        <p>{emailAddress}</p>
        <p>{gender}</p>
        <p>{sexualPreference}</p>
        <p>{age}</p>
        <p>{description}</p>
      </div>
      <Link to={`/user/settings/${id}`}><IconButton><SettingsIcon className="Header-icon" fontSize="large"/></IconButton></Link>
      </div>
    );
  }

app.js

require("dotenv").config();
const express = require('express');
const morgan = require('morgan');
const bodyParser = require('body-parser');
const cors = require('cors');
const cookieParser = require('cookie-parser');
const path = require('path'); 

const enableGlobalErrorLogging = process.env.ENABLE_GLOBAL_ERROR_LOGGING === 'true';

const app = express();

const corsOptions ={
  origin:'http://localhost:3000', 
  credentials:true,            //access-control-allow-credentials:true
  optionSuccessStatus:200
}

app.use(cors(corsOptions));

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({
    extended: true
}));

app.use(cookieParser());

app.use('/uploads', express.static(path.join(__dirname, 'uploads')))

const mongoose = require('mongoose');
const connection =  "password";

mongoose.connect(connection, {
    useNewUrlParser: true,
    useCreateIndex: true,
    useUnifiedTopology: true,
    useFindAndModify: false
});

const userRoutes = require('./routes/userRoutes');

app.use('/', userRoutes);

app.use((req, res, next) => {
  res.header("Access-Control-Allow-Origin", "*");
  res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Authorization, Content-Type, Accept");
  next();
});

// setup a friendly greeting for the root route
app.get('/', (req, res) => {
  res.json({
    message: 'Welcome to the REST API for Tinder!',
  });
 
});

// send 404 if no other route matched
app.use((req, res) => {
  res.status(404).json({
    message: 'Route Not Found',
  });
});

// setup a global error handler
app.use((err, req, res, next) => {
  if (enableGlobalErrorLogging) {
    console.error(`Global error handler: ${JSON.stringify(err.stack)}`);
  }

  res.status(err.status || 500).json({
    message: err.message,
    error: {},
  });
});

app.listen(5000, () => console.log('Listening on port 5000!'))

userRoutes.js

require("dotenv").config();

const express = require("express");
const router = express.Router({ mergeParams: true });
const jwt = require("jsonwebtoken");
const bcryptjs = require("bcryptjs");
const cookieParser = require('cookie-parser'); 
const { check, validationResult } = require("express-validator");

const multer = require('multer');

const User = require("../models/userSchema");

const ObjectID = require('mongodb').ObjectID;

const storage = multer.diskStorage({
  destination: function (req, file, cb) {
    cb(null, 'uploads/')
  },
  filename: function (req, file, cb) {
    cb(null, Date.now() + '.jpg')
  }
})

const upload = multer({ storage: storage }) 

function asyncHandler(callback) {
  return async (req, res, next) => {
    try {
      await callback(req, res, next);
    } catch (error) {
      next(error);
      console.log(error);
    }
  };
}

router.post( "/user/create-account", upload.single("file"), [
    check("firstName")
      .exists({ checkNull: true, checkFalsy: true })
      .withMessage('Please provide a value for "firstName"'),
    check("lastName")
      .exists({ checkNull: true, checkFalsy: true })
      .withMessage('Please provide a value for "username"'),
    check("emailAddress")
      .exists({ checkNull: true, checkFalsy: true })
      .withMessage('Please provide a value for "emailAddress"'),
    check("password")
      .exists({ checkNull: true, checkFalsy: true })
      .withMessage('Please provide a value for "password"'),
    check("gender")
      .exists({ checkNull: true, checkFalsy: true })
      .withMessage('Please provide a value for "gender"'),
    check("sexualPreference")
      .exists({ checkNull: true, checkFalsy: true })
      .withMessage('Please provide a value for "sexualPreference"'),
    check("age")
      .exists({ checkNull: true, checkFalsy: true })
      .withMessage('Please provide a value for "age"'),
    check("description")
      .exists({ checkNull: true, checkFalsy: true })
      .withMessage('Please provide a value for "description"'),
  ],
  asyncHandler(async (req, res, next) => {
    // Attempt to get the validation result from the Request object.
    const errors = validationResult(req);

    // If there are validation errors...
    if (!errors.isEmpty()) {
      // Use the Array `map()` method to get a list of error messages.
      const errorMessages = errors.array().map((error) => error.msg);

      // Return the validation errors to the client.
      return res.status(400).json({ errors: errorMessages });
    }

    const {file, body: { firstName, lastName, emailAddress, password, gender, sexualPreference, age, description}} = req;

    console.log(firstName, lastName, emailAddress, password, gender, sexualPreference, age, description, file);
    
    //new user request body using mongo model from schema
    const postUser = new User({
      firstName: firstName,
      lastName: lastName,
      emailAddress: emailAddress,
      password: password,
      gender: gender,
      sexualPreference: sexualPreference,
      age: age,
      description: description,
      file: file,
      path: req.file.path
    });

    const userEmail = await User.findOne({
      emailAddress: postUser.emailAddress,
    });

    if (postUser.emailAddress === userEmail) {
      console.log("User with this email already exists");

      return res.status(500).end();

    } else if (postUser) {
      //if true salts the password with bcryptjs
      let salt = await bcryptjs.genSalt(10);
      const hashPass = await bcryptjs.hash(postUser.password, salt);
      postUser.password = hashPass;
      postUser.save();

      res.json({ postUser });

      return res.status(201).end();
    } else {
      res.status(400).send({ error: "Error: Account not created" }).end();
    }
  })
);


Solution

  • The image url needs point to a location where the image is served by the backend. You probably don't see the image when you navigate to http://localhost:3000/user/account/frontend/public/uploads/1621968408663.jpg because it doesn't exist at that location.

    First, localhost:3000 is your frontend so you need to change that to point to your backend: localhost:5000.

    Second, you're serving the uploads folder at the /uploads route so everything inside that folder will be available at http://localhost:5000/uploads/.... Therefore you should see the image if you navigate to http://localhost:5000/uploads/1621968408663.jpg.

    So we need to go from:

    http://localhost:3000/user/account/frontend/public/uploads/1621968408663.jpg

    to:

    http://localhost:5000/uploads/1621968408663.jpg.

    When you save the user in userRoutes.js you set User.path to req.file.path which ends up being uploads/1621968408663.jpg. So far so good.

    In AccountPage.js you set image source with <img src={file}/> which is essentially <img src="uploads/1621968408663.jpg"/>. This is where things go wrong. Since this is a relative url, the string is appended to URL on the current page.

    To fix the issue, change the image source to:

    <img src={`http://localhost:5000/${file}`} />