Search code examples
javascriptnode.jsexpressmulter

Accessing nested data with Multer (with Express)


How can I access all images uploaded that are part of an array of objects and save them to /uploads?

My req.body looks like this:

{
  client: 'my_client_test',
  city: 'my_city_test',
  spots: [
    {
      photo_1: 'C:\\fakepath\\photo_1_test.jpeg',
      photo_2: 'C:\\fakepath\\photo_2_test.jpeg',
      address: 'my_address_test'
    }
  ]
}

I need to save all photo_1 and photo_2 of spots array.

const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

const storage = multer.diskStorage({
    destination: function (req, file, cb) {
        cb(null, "uploads/");
    },
    filename: function (req, file, cb) {
        cb(null, file.originalname);
    },
});

const upload = multer({ storage });

app.post("/admin/add", /* HERE */, (req, res) => {
    console.log(req.files);   // undefined
    console.log(req.body);
});

Here are the inputs:

<form id="form" action="/add" enctype="multipart/form-data">
<!-- (...) -->
<div class="spot">
        <label for="photo_1_0">Photo 1:</label>
        <input type="file" id="photo_1_0" name="photo_1_0" required />
        <label for="photo_2_0">Photo 2:</label>
        <input type="file" id="photo_2_0" name="photo_2_0" required />
        <label for="address_0">Address:</label>
        <input type="text" id="address_0" name="address_0" required />
</div>

Inputs are named photo_1_0, photo_2_0 and address_0 because the user is able to click a button and add another spot (photo_1_1, photo_2_1, address_1)

Index.js request

form.addEventListener("submit", (event) => {
    event.preventDefault();
    const client = document.getElementById("client").value;
    const city = document.getElementById("city").value;
    const spotsArray = [];
    for (let i = 0; i < spotIndex; i++) {
        const photo1 = document.getElementById(`photo_1_${i}`).value;
        const photo2 = document.getElementById(`photo_2_${i}`).value;
        const address = document.getElementById(`address_${i}`).value;
        spotsArray.push({ photo_1: photo1, photo_2: photo2, address });
    }
    const data = {
        client,
        city,
        spots: spotsArray,
    };

    console.log(data); // everything looks good here
    fetch("http://localhost:3334/admin/add", {
        method: "POST",
        headers: {
            "Content-Type": "application/json",   // should be "multipart/form-data" ?
        },
        body: JSON.stringify(data),
    });
});

Solution

  • Multer works with multipart/form-data requests. You appear to be sending your request with an application/json body which I cannot recommend for file uploads (even with base64 encoding).

    For something like this, I'd use Multer's any() handler due to the dynamic field names.

    On the client-side, use field names with square-bracket notation to create a structure...

    <label for="photo_1_0">Photo 1:</label>
    <!--        note the field name syntax 👇 -->
    <input type="file" id="photo_1_0" name="spots[0][photo_1]" required />
    
    <label for="photo_2_0">Photo 2:</label>
    <input type="file" id="photo_2_0" name="spots[0][photo_2]" required />
    
    <label for="address_0">Address:</label>
    <input type="text" id="address_0" name="spots[0][address]" required />
    

    then construct a FormData instance to upload your files and other data

    form.addEventListener("submit", async (e) => {
      e.preventDefault();
    
      const body = new FormData(e.target);
    
      const res = await fetch("/admin/add", {
        method: "POST",
        body,
      });
    });
    

    Unfortunately, Multer isn't particularly good at dynamic file field names but you can write a small piece of middleware using Lodash's .set() to make it easier to work with

    import set from "lodash/set.js";
    
    const mapFiles = (req, _res, next) => {
      if (Array.isArray(req.files)) {
        req.files = req.files.reduce(
          (map, { fieldname, ...file }) => set(map, fieldname, file),
          {}
        );
      }
    
      next();
    };
    
    app.post("/admin/add", upload.any(), mapFiles, (req, res) => {
      const bodySpots = req.body.spots; // an array of objects with `address`
      /*
      [
        {
          address: "123 Fake St"
        }
      ]
      */
    
      const fileSpots = req.files.spots; // an array of file pairs
      /*
      [
        {
          photo_1: {
            originalname: ...,
            ...
          },
          photo_2: {
            originalname: ...,
            ...
          }
        }
      ]
      */
    });