Creating an ajax request for file upload form?

I’ve been working on this for weeks. Well, when I get stumped, I implement other features, but this is the core feature of an app I’m working on. I can’t find anything that works. Plenty of solutions out there, but none work for me. Just keep getting 422 errors with this error: {"code":422,"reason":"ValidationError","message":"Missing field","location":"title"} With my code as it sits right now, nothing is getting through. But the values I pass will come back in a console.log.

HTML

            <form role='form' class='form' id='new-challenge'>
              <fieldset>
                <legend>Submit a Challenge</legend>
                <div>
                  <input type='text' name='title' id='challenge-title' class='js-title-input' placeholder='Please enter a title' required>
                  <input type='file' name='image' id='challenge-image' class='js-challenge-upload' accept='image/*' required>
                </div>
                <button aria-label="submit" type="submit">SUBMIT</button>
              </fieldset>
            </form>

Router JS

router.post('/', jwtAuth, parser.single('image'), (req, res) => {
  console.log('>>> req.body: ', req.body);
	const requiredFields = ['title'];
	const missingField = requiredFields.find(field => !(field in req.body));

	if (missingField) {
		return res.status(422).json({
			code: 422,
			reason: 'ValidationError',
			message: 'Missing field',
			location: missingField
		});
	}
	const stringFields = ['title'];
	const nonStringField = stringFields.find(
		field => field in req.body && typeof req.body[field] !== 'string'
	);
	if (nonStringField) {
		return res.status(422).json({
			code: 422,
			reason: 'ValidationError',
			message: 'Incorrect field type: expected string',
			location: nonStringField
		});
	}

	let public_id;

	cloudinary.uploader.upload(req.file.path, result => {
		req.body.image = result.secure_url;
		public_id = result.public_id;

		Challenge.create({
			creator: req.user.id,
			title: req.body.title,
			cloudinary_id: public_id,
			image: CLOUDINARY_BASE_URL + 'image/upload/' + public_id
		})
			.then(challenge => {
				res.status(201).send(challenge.serialize());
			})
			.catch(err => {
				console.error(err);
				res.status(500).json({ error: 'Internal server error' });
			});
	});
});

Client JS

  var files;

  function fileChangeListener() {
    $('input[type=file]').on('change', prepareUpload)
  }

  function prepareUpload() {
    files = event.target.files;
    console.log('staged file: ', files);

  }

function challengeFormSubmit() {
		$('#new-challenge').on('submit', event => {
      event.preventDefault();

      const title = $('#challenge-title').val();

      const formData = new FormData();
      $.each(files, function(key, value){
        console.log('value: ', value)
        formData.append(key, value);
      });

      // const file = document.getElementById('challenge-image').files[0];
	// formData.append('image', file);
	formData.append('title', title);

      console.log('formData: ', formData);

      $.ajax({
        url: '/api/challenges',
        type: 'POST',
        data: formData,
        cache: false,
        dataType: 'json',
        processData: false, 
        ContentType: false, 
        headers: {
          Authorization: `Bearer ${store.authToken}`
        }
      })
  });
}

You can see some comments there where I’ve tried other methods. The console.log shows the file and title correctly (to me). However, formData is coming up as an empty object…

So, to recap: Error 422 is saying title field is missing. formData is not getting appended items. I’ve spent so long on this that I don’t know what to Google search anymore…

You can find the repo here.

edit: It turns out you can’t just console.log FormData.
req.body logs empty object.

I’m trying to keep my api separate, so I have also tried putting my AJAX requests in another file. Here’s what the AJAX looks like for this particular bit:

	const upload = function(path, obj) {
    console.log('obj', obj);

		 return $.ajax({
			type: 'POST',
      url: path,
      enctype: 'multipart/form-data',
      contentType: false, // otherwise, Boundary string will be missing
      dataType: 'json',
      processData: false,
      cache: false,
      data: obj, // also tried JSON.stringify(obj)
      headers: { 
        Authorization: `Bearer ${store.authToken}`,
      },
      success: function(){
        console.log('Success');
      },
      error: function(){
        console.log('Error');
      }
    });
	};

Inside challengeFormSubmit, if I do this:

			const newChallengeTitle = $('.js-title-input').val();
			const file = document.getElementById('image').files[0];

			api
				.upload('/api/challenges', {
					title: newChallengeTitle,
					image: file
				})
  ...

I get the following console.log for obj being used in the API call, I get what appears to be the appropriate data:

{title: "Friends!", image: File(1256106)}
image: File(1256106)
lastModified: 1533491127369
lastModifiedDate: Sun Aug 05 2018 10:45:27 GMT-0700 (Pacific Daylight Time) {}
name: "_DSC5849.png"
size: 1256106
type: "image/png"
webkitRelativePath: ""
__proto__: File
title: "Friends!"

However, it still fails with 422.

For what it’s worth, if I removed the requirement for the title field, I just get an error 500.

What req.body logs?

Oh my gosh. Haha. My bad. It logs an empty object.