1

So I am trying to upload a profile Image via react.js app to a backend running as a REST API using node.js express.js and sqlite as a DB.

But somehow no tutorial I can find will work for me. I must be close though. So what I have right now is this:

My routes look like this:
playerRoutes.js:

const express = require('express');
const router = express.Router();
const multer = require('multer');
const uuidv4 = require('uuid/v4');

const upload_dir = './uploads';

const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, upload_dir);
  },
  filename: (req, file, cb) => {
    cb(null, `${uuidv4()}-${file.filename.toLowerCase}`);
  }
});

const upload = multer({
  storage: storage,
  fileFilter: (req, file, cb) => {
    if (
      file.mimetype == 'image/png' ||
      file.mimetype == 'image/jpg' ||
      file.mimetype == 'image/jpeg'
    ) {
      cb(null, true);
    } else {
      cb(null, false);
      return cb(new Error('Only .png, .jpg and .jpeg format allowed!'));
    }
  }
});

const { playerController } = require('../controller');

router.get('/', playerController.getAllPlayer);
router.get('/:id', playerController.getSpecificPlayer);
router.post('/', upload.single('profileImg'), playerController.createPlayer);
router.put('/:id', playerController.updatePlayer);
router.delete('/:id', playerController.deletePlayer);

module.exports = router;

So key points are the setup of storage which tells where to upload + the file filter in upload, right?

And the route.post which will `upload.single('profileImg'), right? the route will include my controller for create Player which can be found here:

playerController.js

const { Player } = require('../models');

module.exports = {
  getAllPlayer(req, res) {
    return Player.findAll({
      attributes: ['id', 'name', 'nickname', 'profileImg']
    })
      .then(players => res.status(200).send(players))
      .catch(err => res.status(400).send(err));
  },
  getSpecificPlayer(req, res) {
    return Player.findByPk(req.params.id, {
      attributes: ['id', 'name', 'nickname', 'profileImg']
    }).then(player => {
      if (!player) {
        return res.status(404).send({
          message: `Player with id ${req.params.id} not found`
        });
      }
      return res.status(200).send(player);
    });
  },
  createPlayer(req, res) {
    console.log(req.file);
    return Player.create({
      name: req.body.name,
      nickname: req.body.nickname
    })
      .then(player => res.status(201).send(player))
      .catch(err => res.status(400).send(err));
  },
  updatePlayer(req, res) {
    return Player.findByPk(req.params.id, {
      attributes: ['id', 'name', 'nickname', 'profileImg']
    })
      .then(player => {
        if (!player) {
          return res.status(404).send({
            message: `Player with id ${req.params.id} not found`
          });
        }
        return player
          .update({
            name: req.body.name || player.name,
            nickname: req.body.nickname || player.nickname,
            profileImg: req.body.profileImg || player.profileImg
          })
          .then(() => res.status(200).send(player))
          .catch(err => res.status(400).send(err));
      })
      .catch(err => res.status(400).send(err));
  },
  deletePlayer(req, res) {
    return Player.findByPk(req.params.id, {
      attributes: ['id', 'name', 'nickname', 'profileImg']
    })
      .then(player => {
        if (!player) {
          return res.status(404).send({
            message: `Player with id ${req.params.id} not found`
          });
        }
        return player
          .destroy()
          .then(() => res.status(204).send())
          .catch(err => res.status(400).send(err));
      })
      .catch(err => res.status(400).send(err));
  }
};

For now, regarding to several tutorials I should see information within console.log(req.file) when hitting the endpoint with post, right?

Model looks like this:

models/player.js (sequelize model)

'use strict';
module.exports = (sequelize, DataTypes) => {
  const Player = sequelize.define(
    'Player',
    {
      name: {
        type: DataTypes.STRING,
        validate: { notEmpty: { msg: 'Player name cannot be empty' } }
      },
      nickname: {
        type: DataTypes.STRING
      },
      profileImg: {
        type: DataTypes.STRING
      }
    },
    {}
  );
  Player.associate = function(models) {
    // associations can be defined here
  };
  return Player;
};

So now to the frontend code (react.js):

This is the form I am loading in my react app:

playerform.js

import React, { Component } from 'react';
import axios from 'axios';
import {
  FormControl,
  FormGroup,
  FormLabel,
  Form,
  Button
} from 'react-bootstrap';
import PropTypes from 'prop-types';

class PlayerForm extends Component {
  state = {
    name: '',
    nickname: '',
    profileImg: ''
  };

  handleSubmit = e => {
    e.preventDefault();

    axios
      .post(`${process.env.API_URL}/player`, JSON.stringify(this.state), {
        headers: {
          Accept: 'application/json',
          'Content-Type': 'application/json'
        }
      })
      .then(() => this.props.onCreate())
      .catch(err => console.log(err));
  };

  onFileChange(e) {
    this.setState({ profileImg: e.target.files[0] });
  }

  handelChange = e => {
    this.setState({
      [e.target.name]: e.target.value
    });
  };

  render() {
    return (
      <div className="container">
        <Form onSubmit={this.handleSubmit}>
          <FormGroup>
            <FormLabel>Name</FormLabel>
            <FormControl
              type="text"
              name="name"
              placeholder="Enter player name"
              onChange={this.handelChange.bind(this)}
            />
          </FormGroup>
          <FormGroup>
            <FormLabel>Nickname</FormLabel>
            <FormControl
              type="text"
              name="nickname"
              placeholder="Enter player nickname"
              onChange={this.handelChange.bind(this)}
            />
          </FormGroup>
          <FormGroup>
            <FormLabel>Picture</FormLabel>
            <FormControl
              type="file"
              name="file"
              playerholder="Upload player picture"
              onChange={this.onFileChange.bind(this)}
            />
          </FormGroup>
          <Button variant="btn btn-primary" type="submit">
            Add
          </Button>
        </Form>
      </div>
    );
  }
}

PlayerForm.propTypes = {
  onCreate: PropTypes.func.isRequired
};

export default PlayerForm;

So when entering things and hitting the Add Button my api states that req.file is undefined and I cannot find out why.

Can anyone help me drilling down the error?

Patrick Hener
  • 135
  • 5
  • 16

2 Answers2

1

Got it to work. Relevant parts are:

playerform.js:

handleSubmit = e => {
    e.preventDefault();
    const formData = new FormData();
    formData.append('name', this.state.name);
    formData.append('nickname', this.state.nickname);
    formData.append('profileImg', this.state.profileImg);

    axios
      .post(`${process.env.API_URL}/player`, formData, {
        headers: {
          Accept: 'application/json',
          'Content-Type': 'multipart/form-data'
        }
      })
      .then(() => this.props.onCreate())
      .catch(err => console.log(err));
  };

which has to use FormData and as Content-Type 'multipart/form-data'.

Then I had made a mistake with my upload folder, as it was referenced dynamically (wrong) instead of static (had to be './api/uploads')

and then with console.log(req.file) within playerController.js I am finally getting image data.

Patrick Hener
  • 135
  • 5
  • 16
0

You've got a slight mismatch in what multer is expecting, and what you're actually POSTing from your front-end.

If you use Postman (or similar), try sending a POST request as multipart/form-data to your "create player" endpoint. You'll probably see it works.

Your axios post, however, is sending data as 'Content-Type': 'application/json', which the multer package won't handle.

Try utilising the new FormData() construct as discussed here How do I set multipart in axios with react?

RYFN
  • 2,939
  • 1
  • 29
  • 40
  • So I guess I am getting closer. I now get `no such file or directory, open 'uploads/64e5d50f-b0ab-4238-96b8-9a1ebe775748-undefined` when trying to post. it seams like my storage const will not read file.filename. Why? – Patrick Hener Jan 13 '20 at 14:24