I won't waste your time. Chances are if you've ended up here you've run into the same issue that plagued me for 3 months.
Prerequisites:
- You're making a fullstack app using MERN (and using MVC)
- You're trying to send form data that includes image file data in your POST request
- Something is wrong with the incoming request, particularly the image data (parsing the path)
I'm going to assume you have everything else working and you understand your folder structure. I used Axios to send my requests from my React frontend to my backend. You don't need to.
Backend:
-
Install Cloudinary and Multer dependencies (if you haven't already)
npm i cloudinary multer
-
Grab your Cloundiary CLOUD_NAME, API_KEY and API_SECRET from your Cloudinary dashboard.
Navigating to your Cloudinary dashboard:
-
Grab these 3 values and save them in your .env.
-
Set up your Cloudinary middleware file. I named mine cloudinary.js. Here you'll use your keys for the configuration. Make sure you're using the right file path for your project to access those keys in your .env file.
Ex:
const cloudinary = require("cloudinary").v2; require("dotenv").config({ path: "./config/.env" }); cloudinary.config({ cloud_name: process.env.CLOUD_NAME, api_key: process.env.API_KEY, api_secret: process.env.API_SECRET, }); module.exports = cloudinary;
-
Set up your Multer middleware file. Mine is named multer.js and it's in the same 'middleware' folder as my cloudinary.js. I used the following configuration:
const multer = require("multer"); const path = require("path"); module.exports = multer({ storage: multer.diskStorage({}), fileFilter: (req, file, cb) => { let ext = path.extname(file.originalname); if (ext !== ".jpg" && ext !== ".jpeg" && ext !== ".png" && ext !== ".PNG") { req.fileValidationError = "Forbidden extension"; return cb(null, false, req.fileValidationError); // cb(new Error("File type is not supported"), false); // return; } cb(null, true); }, });
-
Set up your POST route for creating a new post/item/whatever, to grab the image file.
*** I used the MVC file structure so my routes are in their own 'routes' folder. All my routes for posts are in a file called 'posts.js'.You'll add upload.single() after the URL in your POST route. Note that I have multer listening for my image data that will come through under the name "file". You can name yours whatever you want but it needs to match what you named that input in your form in the frontend.
const express = require("express"); const router = express.Router(); const postsController = require("../controllers/posts") const upload = require("../middleware/multer") router.post("/addPost", upload.single("file"), postsController.addPost) module.exports = router;
For reference, here's what my input looks like in my React frontend. I've set the name to "file", as well.
-
Set up your controller function for adding the new item. I have my controller functions in a post.js file in a controller folder.
I used destructuring to grab my values. You don't have to do this. You can just use req.body.myProperty, for example.
NOTE: All values that are NOT the file data are preceded with req.body. To grab the file path you can destructure the way I did or you use req.file.path.
const Post = require("../models/Post"); const mongoose = require("mongoose"); const cloudinary = require("../middleware/cloudinary"); module.exports = { addPost: async (req, res) => { const { prompt, media, size, canvas, description } = req.body const { path } = req.file; }, }
-
Right under that, I ran my body of code through a try/catch block. Since you'll need the unique image URL and the CloudinaryID to save to your db, you'll want to do that first and wait for it to return a result.
try{ const result = await cloudinary.uploader.upload(path); }catch(err){ res.status(400).json({err: err.message}) console.error(err) }
-
After awaiting the result from cloudinary, we'll want to create our new post. I have a property in my schema for file, which holds the result.secure_url (this is the unique URL for my image), and cloudinaryId, holding result.public_id -- the id for that image now stored in my media library in cloudinary. I need the id in order to delete later on.
let newPost = await Post.create({ prompt: prompt, media: media, size: size, canvas: canvas, file: result.secure_url,//don't forget to append secure_url to the result from cloudinary cloudinaryId: result.public_id,//append publit_id to this one you need it to delete later description: description, user: req.user.id, });
The whole code block for my addPost function looks like this:
addPost: async (req, res) => { const { prompt, media, size, canvas, description } = req.body const { path } = req.file; try{ const result = await cloudinary.uploader.upload(path); let newPost = await Post.create({ prompt: prompt, media: media, size: size, canvas: canvas, file: result.secure_url,//don't forget to append secure_url to the result from cloudinary cloudinaryId: result.public_id,//append publit_id to this one you need it to delete later description: description, user: req.user.id, }); res.status(200).json(newPost) }catch(err){ res.status(400).json({err: err.message}) console.error(err) } },
Okay, hopefully, that's working for you. I'm sorry if you get red text in the console screaming at you.
Frontend:
-
Install Axios (if you want) Don't forget to cd into your frontend directory. I always make that mistake.
npm i axios
and import it at the top of your component that'll hold the function to send the POST request onSubmit.
import axios from 'axios';
I ended up adding the proxy server URL in my package.json. I was having issues at first and this seemed to resolve it. You'll need to put what you're using for your host and the port as well.
{ "proxy": "http://125.17.80.229:8000" }
-
Make sure you have your form coded out. Here's a simplified example of what I had in my AddPostForm React component:
(I left out the arrays I was mapping through to populate my selection elements)
const AddPostForm = () => { return( <div> <form onSubmit={handleSubmit}> <div> <label htmlFor="prompt">title</label> <input type="text" name="prompt" placeholder="title..." /> </div> <div> <label htmlFor="media">media</label> <div> <select name="media"> {mediaList.map((medium, idx) => ( <option key={idx} value={medium}>{medium}</option> ))} </select> </div> </div> <div> <label htmlFor="size">size</label> <div> <select name="size"> {sizesList.map((size, idx) => ( <option key={idx} value={size}>{size}</option> ))} </select> </div> </div> <div> <label htmlFor="canvas">canvas</label> <div> <select name="canvas"> {canvasList.map((canvas, idx) => ( <option key={idx} value={canvas}>{canvas}</option> ))} </select> </div> </div> <div> <label htmlFor="description">description</label> <textarea type="textarea" name="description" placeholder="Tell us about this piece." ></textarea> </div> <div> <input type="file" name="file"/> </div> <button>submit</button> </form> </div> ) } export default AddPostForm
-
Import useRef from 'react', call it at the top of your component to declare a ref, and add it to your form element.
If you don't know much about the useRef hook and/or have never used it before you can read more about it here. I got this tip from a senior dev who took a look at my code with me. He suggested I use useRef to grab my form data.
import { useRef } from 'react'; const AddPostForm = () => { const formRef = useRef(); return( <form onSubmit={handleSubmit} ref={formRef}> .... </form> ) } export default AddPostForm
-
Declare and define your handleSubmit function.
Create a new FormData object and pass in the ref we declared earlier, appending the current property.
const handleSubmit = async (e) => { e.preventDefault() const formData = new FormData(formRef.current) }
Send and await the result of the POST request. I set mine within a try/catch block. I honestly don't know if it's necessary but I'm so used to doing it now.
The first argument in axios' post() is the URL of your POST route. Make sure yours matches whatever you've set it as.
-
The second argument is the data you want to send off. In this case, the formData we grabbed earlier.
NOTE: With Axios you don't have to append .json() to parse the response as JSON because Axios does that for you.
NOTE2: After getting the response I reset the form data with:
formRef.current.reset();
const handleSubmit = async (e) => { e.preventDefault() const formData = new FormData(formRef.current) try { const res = await axios.post('/post/addPost', formData) console.log(res.data) formRef.current.reset(); }catch (err){ console.log(err.response.data.err) } }
I hope to God this worked for you. Aside from Axios I was also using React's state container library, Redux. Redux holds the state, in this case, my posts, and makes them available globally after wrapping my App.js in a context. It's not necessary to send the form data but it makes it so I don't have to manually refresh the page to see updates to my DB.
I was planning on regularly posting about solutions I find in my own projects but I realized I hate markdown text editors and I never want to deal with getting this to style exactly how I need it to. So this may be my first and last post ever here.
Top comments (0)