How to upload files from a backend (Heroku) to frontend in (Netlify) hosted on github

Viewed 244

I've developed an app that's uploaded to Github and I'm using Heroku to host the (Backend folder) from Github using (automatic deployment) and also using Netlify to host the (Frontend folder) and it's working great in my local computer, but when I try to upload files from my form in frontend it sends a request to the backend and the backend it self saves the file to /uploads folder that's located in frontend directory.

My file structure is like this:

[Server]
- controllers
- - food.js
[Client]
- public
-- uploads

- src
-- pages
--- dashboard
---- food
----- AddFood.js

it's working great on localhost, and this is my code: (client) AddFood.js:

import { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import Axios from 'axios'

import useDocumentTitle from '../../../hooks/useDocumentTitle'

import Modal from '../../../components/Modal/Modal'
import { Success, Error, Loading } from '../../../components/Icons/Status'

import { createSlug } from '../../../functions/slug'
import goTo from '../../../functions/goTo'

const AddFood = () => {
  useDocumentTitle('Add Food')

  //Form States
  const [foodName, setFoodName] = useState('')
  const [foodPrice, setFoodPrice] = useState('')
  const [foodDesc, setFoodDesc] = useState('')

  const [foodFile, setFoodFile] = useState('')
  const [preview, setPreview] = useState()

  const [addFoodStatus, setAddFoodStatus] = useState()
  const [addFoodMessage, setAddFoodMessage] = useState()

  //Form errors messages
  const ImgErr = document.querySelector('[data-form-img-msg]')
  const foodNameErr = document.querySelector('[data-form-name-msg]')
  const priceErr = document.querySelector('[data-form-price-msg]')
  const descErr = document.querySelector('[data-form-desc-msg]')
  const formMsg = document.querySelector('[data-form-msg]')

  const modalLoading = document.querySelector('#modal')
  const BASE_URL =
    process.env.NODE_ENV === 'development'
      ? process.env.REACT_APP_API_LOCAL_URL
      : process.env.REACT_APP_API_URL

  const updateFoodImg = e => {
    const file = e.target.files[0]

    if (file) {
      const fileType = file.type.split('/')[0]
      if (fileType === 'image') setFoodFile(file)

      const fileSizeToMB = file.size / 1000000
      const MAX_FILE_SIZE = 1 //mb

      if (fileSizeToMB > MAX_FILE_SIZE) {
        if (ImgErr)
          ImgErr.textContent = `file size can't be more than ${MAX_FILE_SIZE} MB`
      } else {
        ImgErr.textContent = ''
      }
    }
  }

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

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

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

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

    //using FormData to send constructed data
    const formData = new FormData()
    formData.append('foodName', foodName)
    formData.append('foodPrice', foodPrice)
    formData.append('foodDesc', foodDesc)
    formData.append('foodImg', foodFile)

    if (
      ImgErr.textContent === '' &&
      foodNameErr.textContent === '' &&
      priceErr.textContent === '' &&
      descErr.textContent === ''
    ) {
      //show waiting modal
      modalLoading.classList.remove('hidden')

      try {
        const response = await Axios.post(`${BASE_URL}/foods`, formData)

        const { foodAdded, message } = response.data
        setAddFoodStatus(foodAdded)
        setAddFoodMessage(message)
        //Remove waiting modal
        setTimeout(() => {
          modalLoading.classList.add('hidden')
        }, 300)
      } catch (err) {
        formMsg.textContent = `Sorry something went wrong ${err}`
      }
    } else {
      formMsg.textContent = 'please add all details'
    }
  }

  return (
    <>
      {addFoodStatus === 1 ? (
        <Modal
          status={Success}
          msg='Added food'
          redirectLink='menu'
          redirectTime='3000'
        />
      ) : addFoodStatus === 0 ? (
        <Modal
          status={Error}
          msg={addFoodMessage}
          msg=''
        />
      ) : null}

      <section className='py-12 my-8 dashboard'>
        <div className='container mx-auto'>
          <h3 className='mx-0 mt-4 mb-12 text-2xl text-center'>Add food</h3>
          <div>
            <div className='food'>
              {/* Show Modal Loading when submitting form */}
              <Modal
                status={Loading}
                modalHidden='hidden'
                classes='text-blue-500 text-center'
                msg='Please wait'
              />

              <form
                method='POST'
                className='form'
                encType='multipart/form-data'
                onSubmit={handleAddFood}
              >
                <label className='flex flex-wrap items-center justify-center gap-4 mb-8 sm:justify-between'>
                  <img
                    src={
                      preview === null
                        ? 'https://source.unsplash.com/random?food'
                        : preview
                    }
                    alt='food' //change with food image name
                    className='object-cover p-1 border border-gray-400 w-28 h-28 dark:border-gray-300 rounded-xl'
                  />
                  <input
                    type='file'
                    name='foodImg'
                    id='foodImg'
                    accept='image/*'
                    onChange={updateFoodImg}
                    className='grow-[.7] cursor-pointer text-lg text-white p-3 rounded-xl bg-orange-800 hover:bg-orange-700 transition-colors'
                    required
                  />
                  <span
                    className='inline-block my-2 text-red-400 font-[600]'
                    data-form-img-msg
                  ></span>
                </label>

                <label htmlFor='foodName' className='form-group'>
                  <input
                    type='text'
                    id='foodName'
                    className='form-input'
                    autoFocus
                    required
                    onChange={e => setFoodName(createSlug(e.target.value.trim()))}
                  />
                  <span className='form-label'>Food Name</span>
                  <span
                    className='inline-block my-2 text-red-400 font-[600]'
                    data-form-name-msg
                  ></span>
                </label>

                <label htmlFor='foodPrice' className='form-group'>
                  <input
                    type='number'
                    id='foodPrice'
                    className='form-input'
                    min='5'
                    max='500'
                    required
                    onChange={e => setFoodPrice(e.target.value.trim())}
                  />
                  <span className='form-label'>Price</span>
                  <span
                    className='inline-block my-2 text-red-400 font-[600]'
                    data-form-price-msg
                  ></span>
                </label>

                <label htmlFor='foodDescription' className='form-group'>
                  <textarea
                    name='foodDescription'
                    id='foodDescription'
                    minLength='10'
                    maxLength='300'
                    className='form-input'
                    required
                    onChange={e => setFoodDesc(e.target.value.trim())}
                  ></textarea>
                  <span className='form-label'>Description</span>
                  <span
                    className='inline-block my-2 text-red-400 font-[600]'
                    data-form-desc-msg
                  ></span>
                </label>

                <div
                  className='my-14 text-red-400 font-[600] text-center text-xl'
                  data-form-msg
                ></div>

                <div className='flex items-center justify-evenly'>
                  <button
                    type='submit'
                    className='min-w-[7rem] bg-green-600 hover:bg-green-700 text-white py-1.5 px-6 rounded-md'
                  >
                    Add
                  </button>
                  <Link
                    to={goTo('menu')}
                    className='text-gray-800 underline-hover text-bold dark:text-white'
                  >
                    Food Menu
                  </Link>
                </div>
              </form>
            </div>
          </div>
        </div>
      </section>
    </>
  )
}

export default AddFood

(server) foods.js:

const FoodsModel = require(`${__dirname}/../models/food-model.js`)
const { v4: uuidv4 } = require('uuid')
const sharp = require('sharp')
const deleteFile = require('../functions/deleteFile')

const addFood = async (req, res) => {
  const { foodName, foodPrice, foodDesc } = req.body
  const { foodImg } = req.files
  const foodImgName = uuidv4() + foodImg.name
  const foodImgMovePath = `${__dirname}/../../client/public/uploads/${foodImgName}.webp`
  const foodImgDisplayPath = `/uploads/${foodImgName}`

  const foods = new FoodsModel({
    foodImgDisplayPath,
    foodName,
    foodPrice,
    foodDesc
  })

  sharp(foodImg.data)
    .rotate()
    .resize(200)
    .jpeg({ mozjpeg: true, quality: 50 })
    .toBuffer()
    .then(newBuffer => {
      //changing the old jpg image buffer to new webp buffer
      foodImg.data = newBuffer

      foodImg.mv(foodImgMovePath, err => {
        if (err) {
          res.json({
            message: `Sorry something wrong with server! : ${err}`
          })
          return
        }

        foods.save()

        res.json({
          message: 'Food Added Successfully',
          foodAdded: 1
        })
      })
    })
    .catch(err => {
      res.json({
        //https://mhmdhidr-restaurant.netlify.app/uploads/20cc09a0-1811-48b0-bffa-49e7a1981537chicken-legs.webp
        message: `Sorry! Something went wrong, check the error => : \n ${err}`,
        foodAdded: 0
      })
    })
}

const getFood = async (req, res) => {
  res.json(res.paginatedResults)
}

const deleteFood = async (req, res) => {
  const { prevFoodImg } = req.body
  const { foodId } = req.params

  deleteFile(prevFoodImg)

  try {
    await FoodsModel.findByIdAndRemove(foodId)
    res.json({
      message: 'Food Deleted Successfully',
      foodDeleted: 1
    })
  } catch (error) {
    res.json({
      message: `Sorry! Something went wrong, check the error => : \n ${error}`,
      foodDeleted: 0
    })
  }
}

const updateFood = async (req, res) => {
  const { foodName, foodPrice, foodDesc, prevFoodImg } = req.body
  const { foodId } = req.params

  const { foodImg } = req.files || ''
  const foodImgName = uuidv4() + foodImg?.name || ''
  const foodImgMovePath = `${__dirname}/../../client/public/uploads/${foodImgName || ''}`
  const foodImgDisplayPath =
    foodImg !== '' && foodImg !== undefined ? `/uploads/${foodImgName}` : prevFoodImg

  try {
    await FoodsModel.findByIdAndUpdate(foodId, {
      foodImgDisplayPath,
      foodName,
      foodPrice,
      foodDesc
    })

    if (foodImg !== '' && foodImg !== undefined) {
      deleteFile(prevFoodImg)

      foodImg.mv(foodImgMovePath, err => {
        if (err) {
          res.json({ message: `Sorry something wrong with server! : ${err}` })
        }
      })
    }

    res.json({
      message: 'Food Updated Successfully',
      foodUpdated: 1
    })
  } catch (error) {
    res.json({
      message: `Sorry! Something went wrong, check the error => : \n ${error}`,
      foodUpdated: 0
    })
  }
}

module.exports = { addFood, getFood, deleteFood, updateFood }

But when I try to upload a file in the Netlify app this error shows up:

Error: ENOENT: no such file or directory, open '/app/controllers/../../client/public/uploads/9631bb96-e41d-4c9a-aa35-d22b551ab662MASHAWI-IN-DUBAI.jpeg.webp'

I've tried to Google it a lot but unfortunately didn't find a solution.

Thanks for your help.

1 Answers

A two part answer:

  • Your back-end has no business putting files into your front-end's directory structure.

    A better choice might be to use an uploads/ folder in the back-end project, exposing those over HTTPS, and linking to them from your front-end.

  • But that won't work on Heroku due to its ephemeral filesystem.

    An even better choice would be to save them to a cloud-based object store like Amazon S3 or Azure Blob Storage, or a more specialized service like Cloudinary if they're images. Heroku tends to recommend S3.

    Your back-end now just needs to store the URL to each file and provide that link to your front-end upon request.

    Even on other hosts that allow you to save files into your back-end's filesystem, using a third-party service has many benefits. You can trivially scale horizontally (adding new nodes), your application becomes less stateful, etc.

User uploads never belong in your code repository, no matter how and where you choose to host them. They are content, not code, and should not be tracked and versioned alongside your code.

Related