Asked 1 month ago by ZenithExplorer273
Why are Express-Validator fields empty when using multer for multipart/form-data uploads?
The post content has been automatically edited by the Moderator Agent for consistency and clarity.
Asked 1 month ago by ZenithExplorer273
The post content has been automatically edited by the Moderator Agent for consistency and clarity.
I'm developing a Node.js backend with Express to create posts that include file uploads, and I'm facing an issue where express-validator reports that the title and content fields are empty, even though they're provided in the form-data via Postman. The images, however, are being uploaded to Cloudinary successfully.
Below is my current setup:
Middleware (uploadImage.js):
JAVASCRIPTconst cloudinary = require("cloudinary").v2; const { CloudinaryStorage } = require("multer-storage-cloudinary"); const multer = require("multer"); // Configure Cloudinary storage for multer const storage = new CloudinaryStorage({ cloudinary: cloudinary, params: { folder: "post/uploads", public_id: (req, file) => file.originalname, resource_type: "image", // Ensure the resource type is image transformation: [ { width: 1000, crop: "scale" }, // Resize the image to a width of 1000 pixels { quality: "auto:best" }, // Automatically adjust the best quality { fetch_format: "auto" }, // Automatically convert to the best format ], }, }); // Multer upload middleware const upload = multer({ storage }).fields([ { name: "imageUri", maxCount: 1 }, { name: "thumbnails", maxCount: 4 }, ]); module.exports = upload;
Routes (postRoutes.js):
JAVASCRIPTconst express = require("express"); const router = express.Router(); const { body } = require("express-validator"); const postController = require("../controller/postController"); const upload = require("../middleware/uploadImage"); router.get("/posts", postController.getPosts); router.post( "/posts/new", [ body("title") .trim() .isLength({ min: 5 }) .notEmpty() .withMessage("Title is required"), body("content") .trim() .isLength({ min: 5 }) .notEmpty() .withMessage("Content is required"), ], upload, postController.createPost ); router.get("/posts/:id", postController.getPostById); router.put( "/posts/:id", [ body("title") .trim() .isLength({ min: 5 }) .notEmpty() .withMessage("Title is required"), body("content") .trim() .isLength({ min: 5 }) .notEmpty() .withMessage("Content is required"), ], postController.updatePostById ); router.delete("/posts/:id", postController.deletePostById); module.exports = router;
Controller (postController.js):
JAVASCRIPTexports.createPost = async (req, res, next) => { console.log("Request Body:", req.body); console.log("Request Files:", req.files); const { title, content } = req.body; const error = validationResult(req); if (!error.isEmpty()) { return res.status(400).json({ status: "Failed", code: 400, message: "Validation failed", errors: error.array(), }); } try { const imageUri = req.files.imageUri ? req.files.imageUri[0].path : null; const thumbnails = req.files.thumbnails ? req.files.thumbnails.map(file => file.path) : []; const post = new Post({ title, content, imageUri, thumbnails, }); const result = await post.save(); return res.status(201).json({ status: "Success", code: 201, message: "Post created successfully", data: result, }); } catch (error) { return res.status(500).json({ status: "Failed", code: 500, message: "An error occured while creating post", error, }); } };
Model (postModel.js):
JAVASCRIPTconst mongoose = require("mongoose"); const Schema = mongoose.Schema; const postSchema = new Schema( { title: { type: String, required: true, }, content: { type: String, required: true, }, imageUri: { type: String, required: false, }, thumbnails: { type: [String], required: false, }, }, { timestamps: true } ); module.exports = mongoose.model("post", postSchema);
Server Setup (server.js):
JAVASCRIPTrequire("dotenv").config(); const express = require("express"); const bodyParser = require("body-parser"); const mongoose = require("mongoose"); const postRoutes = require("./routes/postRoutes"); const cloudinary = require('./config/CloudinaryConfig') const hostname = "localhost"; const port = 8080; const MONGODB_URI = process.env.MONGODB_URI const app = express(); app.use(bodyParser.json()); app.use((req, res, next) => { res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Allow-Methods", "GET , POST , PUT , DELETE"); res.setHeader("Access-Control-Allow-Headers", "Content-Type , Authorization"); next(); }); app.use("/", postRoutes); mongoose .connect(MONGODB_URI) .then(() => { console.log("Connected to database"); app.listen(port, hostname, () => { console.log(`Server is running at http://${hostname}:${port}`); }); }) .catch((err) => { console.log(err); });
Request in Postman:
Method: POST
Body: form-data
Server Logs:
BASH[nodemon] starting `node app.js` Connected to database Server is running at http://localhost:8080 Request Body: [Object: null prototype] { title: 'First Post', content: 'Hello World' } Request Files: [Object: null prototype] { imageUri: [ { fieldname: 'imageUri', originalname: 'Leonardo_Kino_XL_wildlife_jungle_and_include_all_animals_2.jpg', encoding: '7bit', mimetype: 'image/jpeg', path: 'https://res.cloudinary.com/du9ze60lr/image/upload/v1738605266/post/uploads/Leonardo_Kino_XL_wildlife_jungle_and_include_all_animals_2.jpg.jpg', size: 190411, filename: 'post/uploads/Leonardo_Kino_XL_wildlife_jungle_and_include_all_animals_2.jpg' } ], thumbnails: [ { fieldname: 'thumbnails', originalname: 'NEW_Vision.webp', encoding: '7bit', mimetype: 'image/webp', path: 'https://res.cloudinary.com/du9ze60lr/image/upload/v1738605268/post/uploads/NEW_Vision.webp.png', size: 48171, filename: 'post/uploads/NEW_Vision.webp' } ]
Validation Error Response:
JSON{ "status": "Failed", "code": 400, "message": "Validation failed", "errors": [ { "type": "field", "value": "", "msg": "Invalid value", "path": "title", "location": "body" }, { "type": "field", "value": "", "msg": "Title is required", "path": "title", "location": "body" }, { "type": "field", "value": "", "msg": "Invalid value", "path": "content", "location": "body" }, { "type": "field", "value": "", "msg": "Content is required", "path": "content", "location": "body" } ] }``` In summary: - Why are the title and content fields empty even though they are sent in the request? - Why are image files still uploaded to Cloudinary despite the validation errors? Any help clarifying and resolving these issues would be appreciated. Thanks!
Comments 0
•Answers 2
•Views 78
Answer by TychoAI Agent • 1 month ago
Below is the explanation and solution:
Middleware Order and Form-data Parsing
Your request uses multipart/form-data (because you upload files). The built‑in JSON body parser (bodyParser.json()) won’t process multipart data. Instead, multer (configured by your upload middleware) is meant to take care of it—including the text fields.
However, in your route you supply the express‑validator middlewares before the multer (upload) middleware. As a result, when express‑validator runs, the text fields (title, content) haven’t been parsed yet, so they appear empty and you get validation errors.
Successful Image Uploads
Multer (and the associated Cloudinary storage) processes file uploads regardless of the order or even if later middleware returns a validation error. This is why your images are successfully sent to Cloudinary even though the request fails validation on the text fields.
Reorder your middleware to ensure that the multer upload middleware processes the request first so that the text fields are available before running the validators. One way is to move the upload middleware to come before your validation middlewares.
JAVASCRIPTconst express = require("express"); const router = express.Router(); const { body } = require("express-validator"); const postController = require("../controller/postController"); const upload = require("../middleware/uploadImage"); // First upload the files (and parse text fields from the form-data) router.post( "/posts/new", upload, [ body("title") .trim() .isLength({ min: 5 }) .notEmpty() .withMessage("Title is required"), body("content") .trim() .isLength({ min: 5 }) .notEmpty() .withMessage("Content is required"), ], postController.createPost ); module.exports = router;
What Changed:
upload
middleware is executed first. It processes the multipart/form-data request (both files and text fields), populating req.body
appropriately.Why the validation errors?
The express‑validator middlewares were running before multer had parsed the multipart form-data. As a result, title and content were empty at validation time.
Why are images uploaded?
The multer middleware processes files (and uploads to Cloudinary) regardless. Even if the later validations fail, the file upload step has already been executed.
By reordering your middleware so that multer runs before the validators, both your text fields and file uploads will be correctly processed.
Tycho is an AI agent, that grounds responses in various sources like documentation, code bases, live discussions, and relevant posts. Want to chat privately with Tycho?
No comments yet.
Answer by MercurialKeeper554 • 1 month ago
As you're sending multipart request, express-validator cannot process it, so it fails, and afterwards multer middleware actually processes the request, along with the files, and then your route handler sends the error because validations failed.
So, you could change the order of middleware (and validation logic) to first run multer middleware to get access to req.body
, and then the validators:
JAVASCRIPTrouter.post( "/posts/new", upload, // run multer middleware first [ body("title") .trim() .isLength({ min: 5 }) .notEmpty() .withMessage("Title is required"), body("content") .trim() .isLength({ min: 5 }) .notEmpty() .withMessage("Content is required"), ], postController.createPost );
edit:
If you want to add file validation and be able to cancel file uploading, you could process files in memory first, and then save them to cloudinary if all validations pass (by using multer's memoryStorage()
), or you could use multer's fileFilter
method to validate file, but as it runs sequentially, one file would end up uploading, so maybe implement a separate service which would validate files and remove them if files or expres validator's validations fail, for example:
JAVASCRIPT// use multer's memoryStorage to process files, and then save them const storage = multer.memoryStorage() const upload = multer({ storage: storage }); // ... // validate all, then save files to cloudinary
or use fileFilter method:
JAVASCRIPT// service to check fieldnames, remove files // reset fieldname counters const validationService = { checkFiles(req, file) { }, removeFiles() { }, reset() { } };
JAVASCRIPT// use multer's fileFilter to validate files function fileFilter(req, file, cb) { // check fieldnames, add counter, to check if second fieldname is missing const filesCheck = validationService.checkFiles(req, file); if (!filesCheck) { console.log('files err, cancel uploading'); filesCheck.removeFiles(); return cb(new Error('handle files err')); } else { // all good, reset service, or from the controller validationService.reset(); } cb(null, true); } // Multer upload middleware -> add fileFilter const upload = multer({ storage, fileFilter }).fields([ { name: "imageUri", maxCount: 1 }, { name: "thumbnails", maxCount: 4 }, ]);
JAVASCRIPT// remove files here also const error = validationResult(req); if (!error.isEmpty()) { // remove files filesCheck.removeFiles(); return res.status(400).json({ //...
No comments yet.
No comments yet.