I have a form to add hotel in the database. I am using react-hook-form. In the form, I am asking images as an input as well. Upon submitting the form, the data from the form is converted into form data and then sent to the API.
This is where the conversion is happening and form is being submitted.
This is my API call:
const formMethods = useForm<HotelFormData>();
//we are not gonna destruct the formMethods object here
const { handleSubmit } = formMethods;
const onSubmit = handleSubmit((data: HotelFormData) => {
console.log(data);
//We are going to send the data to the server, but first we need to convert the data to FormData because we have imageFiles in the data and we can not send it as a JSON object
const formData = new FormData();
formData.append("name", data.name);
formData.append("city", data.city);
formData.append("country", data.country);
formData.append("description", data.description);
formData.append("type", data.type);
formData.append("pricePerNight", data.pricePerNight.toString());
formData.append("starRating", data.starRating.toString());
formData.append("adultCapacity", data.adultCapacity.toString());
formData.append("childCapacity", data.childCapacity.toString());
data.facilities.forEach((facility, index) => {
formData.append(`facilities[${index}]`, facility);
});
// for (let i = 0; i < data.imageFiles.length; i++) {
// formData.append("imageFiles", data.imageFiles[i]);
// }
Array.from(data.imageFiles).forEach((imageFile) => {
formData.append("imageFiles", imageFile);
});
onSave(formData);
console.log(formData);
});
export const addMyHotel = async (formData: FormData) => {
console.log("formData",formData)
const response = await fetch(`${BASE_URL}/api/my-hotels`, {
method: "POST",
credentials: "include", //to send the cookie
body: formData,
});
const resBody = await response.json();
if (!response.ok) {
throw new Error(resBody.message);
}
return resBody;
};
This is my api/my-hotels
:
import express from "express";
import { Request, Response } from "express";
import { PutObjectCommand } from "@aws-sdk/client-s3";
import mime from "mime-types";
import { client } from "../index";
import multer from "multer";
import Hotel, { HotelType } from "../models/hotel";
import verifyToken from "../middleware/auth";
import { body, validationResult } from "express-validator";
const router = express.Router();
const storage = multer.memoryStorage();
const upload = multer({
storage: storage,
limits: {
fileSize: 5 * 1024 * 1024, // 5MB
},
});
//this will be the root
router.post(
"/",
verifyToken, //only logged in users can create hotels
[
body("name").notEmpty().withMessage("Name is required"),
body("city").notEmpty().withMessage("City is required"),
body("country").notEmpty().withMessage("Country is required"),
body("description").notEmpty().withMessage("Description is required"),
body("type").notEmpty().withMessage("Type is required"),
body("pricePerNight")
.notEmpty()
.isNumeric()
.withMessage("Price per night must be a number"),
body("adultCapacity")
.notEmpty()
.isNumeric()
.withMessage("Adult capacity must be a number"),
body("childCapacity")
.notEmpty()
.isNumeric()
.withMessage("Child capacity must be a number"),
body("starRating")
.notEmpty()
.isNumeric()
.withMessage("Star rating must be a number"),
body("facilities")
.notEmpty()
.isArray()
.withMessage("Facilities must be an array"),
],
upload.array("imageFiles", 6),
async (req: Request, res: Response) => {
console.log("req body");
console.log(req.body);
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res
.status(400)
.json({ message: "Bad Request", errors: errors. array() });
}
try {
const files = req.files as Express.Multer.File[];
const promises = files.map(async (file) => {
const fileExt = mime.extension(file.mimetype);
console.log(file);
// const fileName = file.originalname;
if (!fileExt) {
throw new Error("Invalid file type");
}
const newFileName = `${Date.now()}`;
const params = {
Bucket: process.env.S3_BUCKET as string,
Key: `${Date.now()}.${fileExt}`,
Body: file.buffer,
ContentType: file.mimetype,
};
await client.send(new PutObjectCommand(params));
const link = `https://${process.env.S3_BUCKET_NAME}.${process.env.S3_REGION}.amazonaws.com/${newFileName}`;
return link;
});
const imageUrls = await Promise.all(promises);
const newHotel: HotelType = req.body; //some of the fields will already be filled by the req body
newHotel.imageUrls = imageUrls;
newHotel.lastUpdated = new Date();
newHotel.userId = req.userId; //this is taken from the auth token
//save the hotel to the database
const hotel = new Hotel(newHotel);
await hotel.save();
res.status(201).send(hotel); //201 is the status code for created
res.status(201).json({ message: "Images uploaded successfully" });
} catch (error) {
console.log("error creating hotel", error);
res.status(500).json({ message: "Internal server error" });
}
}
);
export default router;
Now, when I submit the form I get the following errors:
Bad Request - errors:
0: {type: "field", msg: "Name is required", path: "name", location: "body"}
1: {type: "field", msg: "City is required", path: "city", location: "body"}
2: {type: "field", msg: "Country is required", path: "country", location: "body"}
3: {type: "field", msg: "Description is required", path: "description", location: "body"}
4: {type: "field", msg: "Type is required", path: "type", location: "body"}
5: {type: "field", msg: "Invalid value", path: "pricePerNight", location: "body"}
6: {type: "field", msg: "Price per night must be a number", path: "pricePerNight", location: "body"}
7: {type: "field", msg: "Invalid value", path: "adultCapacity", location: "body"}
8: {type: "field", msg: "Adult capacity must be a number", path: "adultCapacity", location: "body"}
9: {type: "field", msg: "Invalid value", path: "childCapacity", location: "body"}
10: {type: "field", msg: "Child capacity must be a number", path: "childCapacity", location: "body"}
11: {type: "field", msg: "Invalid value", path: "starRating", location: "body"}
12: {type: "field", msg: "Star rating must be a number", path: "starRating", location: "body"}
13: {type: "field", msg: "Invalid value", path: "facilities", location: "body"}
14: {type: "field", msg: "Facilities must be an array", path: "facilities", location: "body"}
The content type in my headers is set to application/json; charset=utf-8
.
This is the payload of the request:
name: My Hotel
city: Iwdj
country: Pdihuf
description: poiuytrewqasdfghjkllllllllllllllllkmnbvcxz
type: hotel
pricePerNight: 7
starRating: 3
adultCapacity: 8
childCapacity: 7
facilities[0]: free parking
facilities[1]: swimming pool
imageFiles: (binary)
imageFiles: (binary)
I tried to manually set the content type to multipart/formdata
but then I got an error
Multipart: Boundary not found
As the request is of multipart/form-data
content type, express validator cannot process it, because it runs before multer processes the request, so it receives empty body, hence every validation fails (it probably tries to process the request as JSON, not sure where JSON headers are coming from, though.., but it seems when you log req.body
later, it's already processed by multer (you might move validator to separate middleware, so that it doesn't reach route handler/next middleware, see: Example: creating own validation runner).
So, move multer middleware before express validator middleware, so that it recieves processed body
object (fronted request looks fine, no need to set content type, it's being handled by itself, the boundary error is what you get when trying to set it manually):
router.post(
"/",
verifyToken,
// multer needs to process request first to obtain req.body
upload.array("imageFiles", 6),
[
body("name").notEmpty().withMessage("Name is required"),
//...
],
async (req: Request, res: Response) => {
console.log("req body");
console.log(req.body);