Search code examples
node.jsreactjsmongoosemultipartform-dataform-data

How to send formData that includes image


I'm trying to send my form using React frontend to NodeJS backend and I'm not getting any data in formData() object, here is my code for React:

import { useState, useEffect } from 'react'
import Axios from 'axios'
import Notification from '../../../components/Notification'

const DashboardWorkAdd = ({ subTitle = 'works' }) => {
  //Form States
  const [workName, setWorkName] = useState('')
  const [workGithub, setWorkGithub] = useState('')
  const [workOnlineLink, setWorkOnlineLink] = useState('')
  const [workDate, setWorkDate] = useState('')
  const [workDesc, setWorkDesc] = useState('')
  const [workAdded, setworkAdded] = useState('')
  const [workAddedMsg, setworkAddedMsg] = useState('')

  const [workImgFile, setWorkImgFile] = useState('')
  const [preview, setPreview] = useState()

  const formMsg = document.querySelector('.notification__msg')

  const updateWorkImg = (e) => {
    const file = e.target.files[0]

    if (file) {
      const fileType = file.type.split('/')[0]
      const fileSizeToMB = file.size / 1000000
      const MAX_FILE_SIZE = 1 //mb

      if (formMsg) {
        formMsg.classList.remove('hidden')

        if (fileType !== 'image') {
          formMsg.textContent = 'you can add only image file'
        } else if (fileSizeToMB > MAX_FILE_SIZE) {
          formMsg.textContent = `you can't add more than ${MAX_FILE_SIZE} MB`
          return
        } else {
          formMsg.classList.add('hidden')
          formMsg.textContent = ''
          setWorkImgFile(file)
        }
      }
    }
  }

  useEffect(() => {
    // if there's an image
    if (workImgFile) {
      const reader = new FileReader()

      reader.onloadend = () => setPreview(reader.result)

      reader.readAsDataURL(workImgFile)
    } else {
      setPreview(null)
    }
  }, [workImgFile])

  const handleAddWork = async (e) => {
    e.preventDefault()

    //using FormData to send constructed data
    const formData = new FormData()
    formData.append('workImg', workImgFile)
    formData.append('workName', workName)
    formData.append('workGithub', workGithub)
    formData.append('workOnlineLink', workOnlineLink)
    formData.append('workDate', workDate)
    formData.append('workDesc', workDesc)

    console.log(formData)

    if (
      (workName === '' || workGithub === '' || workOnlineLink === '' || workDate === '',
      workDesc === '')
    ) {
      formMsg.textContent = 'please add all data'
    } else {
      try {
        const response = await Axios.post(
          `${
            process.env.NODE_ENV === 'development'
              ? process.env.REACT_APP_API_LOCAL_URL
              : process.env.REACT_APP_API_URL
          }/workAdd`,
          { workImgFile, workName, workGithub, workOnlineLink, workDate, workDesc }
        )

        const { workAdded, message } = response.data
        setworkAdded(workAdded)
        setworkAddedMsg(message)
      } catch (err) {
        console.error(err)
      }
    }
  }

  return (
    <>
      <h3 className='text-3xl mt-20 mb-12 text-center font-semibold'>{subTitle}</h3>

      <div className='h-full'>
        <div className='flex flex-col gap-3 py-4 text-sm font-semibold'>
          <Notification sendStatus={workAdded} sendStatusMsg={workAddedMsg} />
          <div className='notification__msg'></div>
          <form
            method='POST'
            className='flex flex-col gap-14'
            encType='multipart/form-data'
            onSubmit={handleAddWork}
          >
            <label
              htmlFor='workImg'
              className='flex flex-wrap justify-center gap-5 md:justify-between items-center cursor-pointer'
            >
              <img
                src={
                  preview === null ? 'https://source.unsplash.com/random?webdev' : preview
                }
                alt='Work Portfolio Preview'
                className='w-36 h-36'
              />
              <input
                type='file'
                name='workImg'
                id='workImg'
                className='bg-blue-500 py-6 px-28 rounded-lg text-white uppercase font-semibold cursor-pointer'
                accept='image/*'
                onChange={updateWorkImg}
                multiple
                required
              />
            </label>
            <div className='dashboard-group'>
              <label htmlFor='workName'>work name</label>
              <input
                type='text'
                id='workName'
                autoFocus
                onChange={(e) => {
                  setWorkName(e.target.value.trim())
                }}
                required
              />
            </div>
            <div className='dashboard-group'>
              <label htmlFor='workLinkGithub'>Github Link</label>
              <input
                type='text'
                id='workLinkGithub'
                min='5'
                max='500'
                onChange={(e) => setWorkGithub(e.target.value.trim())}
                required
              />
            </div>
            <div className='dashboard-group'>
              <label htmlFor='workLinkOnline'>Online Linl</label>
              <input
                type='text'
                id='workLinkOnline'
                min='5'
                max='500'
                onChange={(e) => setWorkOnlineLink(e.target.value.trim())}
                required
              />
            </div>
            <div className='dashboard-group'>
              <label htmlFor='workDate'>Work Date</label>
              <input
                type='date'
                id='workDate'
                min='5'
                max='500'
                onChange={(e) => setWorkDate(e.target.value.trim())}
                required
              />
            </div>
            <div className='dashboard-group'>
              <label htmlFor='workDescription'>word description</label>
              <textarea
                name='workDescription'
                id='workDescription'
                minLength='10'
                maxLength='300'
                className=''
                onChange={(e) => setWorkDesc(e.target.value.trim())}
                required
              ></textarea>
            </div>
            <div className='flex justify-around text-lg'>
              <button
                type='submit'
                className='bg-green-500 hover:bg-green-600 py-2 px-20 rounded-lg text-white transition-colors'
              >
                Add
              </button>
            </div>
          </form>
        </div>
      </div>
    </>
  )
}

export default DashboardWorkAdd

Here is my code for NodeJS:

const WorksModel = require('../models/WorkModel')
const { v4: uuidv4 } = require('uuid')

module.exports = async (req, res) => {
  const { workName, workGithub, workOnlineLink, workDate, workDesc } = req.body
  const { workImg } = req.files

  const workImgName = uuidv4() + workImg.name

  const workImgMovePath = `${__dirname}/../../client/public/uploads/${workImgName}`
  const workImgDisplayPath = `/uploads/${workImgName}`

  const works = new WorksModel({
    workImgDisplayPath,
    workName,
    workGithub,
    workOnlineLink,
    workDate,
    workDesc
  })

  try {
    await works.save()
    workImg.mv(workImgMovePath, (err) => {
      if (err) {
        res.send({ message: `sorry, something wrong with the server: ${error}` })
        return res.status(500).send(err)
      }
    })

    res.send({
      message: 'added succesfully',
      workAdded: 1
    })
  } catch (error) {
    res.send({
      message: `something went wrong ${error}`,
      workAdded: 0
    })
  }
}

and my WorkModel file is :

const mongoose = require('mongoose')

const WorkSchema = new mongoose.Schema({
  workImgDisplayPath: {
    type: String,
    required: true
  },
  workName: {
    type: String,
    required: true
  },
  workGithub: {
    type: String,
    required: true
  },
  workOnlineLink: {
    type: String,
    required: true
  },
  workDate: {
    type: Date,
    required: true,
    default: new Date().toLocaleDateString('ar-EG', {
      weekday: 'long',
      year: 'numeric',
      month: 'short',
      day: 'numeric'
    })
  },
  workDesc: {
    type: String,
    required: true
  }
})

const WorkModel = mongoose.model('mhmdhidrPortfolio', WorkSchema)

module.exports = WorkModel

the issue I'm having when I get the data in the backend is the req.files are undefined, I googled a lot and I don't know the issue exactly.

Thanks for your help.


Solution

  • I solved my problem, I just had to add:

    const fileUpload = require('express-fileupload')
    
    app.use(express.json())
    app.use(express.urlencoded({ extended: true }))
    app.use(fileUpload())
    

    inside my index.js (server) file