1

I am working on a blogging application (click the link to see the GitHub repo) with Express, EJS and MongoDB.

Before submitting a new post, of course, I have to validate the form entries. I use express-validator version 6.3.0.

My addPost controller:

exports.addPost = (req, res, next) => {
    // Form validation rules
    check('title', 'The title field id required')
        .not()
        .isEmpty();
    check('excerpt', 'The excerpt field id required')
        .not()
        .isEmpty();
    check('body', 'The full text field id required')
        .not()
        .isEmpty();

    const errors = validationResult(req);

    if (!errors.isEmpty()) {
        console.log(errors.array());
    }

    if (!errors.isEmpty()) {
        res.render('admin/addpost', {
            layout: 'admin/layout',
            website_name: 'MEAN Blog',
            page_heading: 'Dashboard',
            page_subheading: 'Add New Post',
            errors: errors
        });
        req.flash('danger', errors);
        req.session.save(() => res.redirect('/dashboard'));
    } else {
        const post = new Post();
        post.title = req.body.title;
        post.short_description = req.body.excerpt
        post.full_text = req.body.body;

        post.save(function(err) {
            if (err) {
                console.log(err);
                return;
            } else {
                req.flash('success', "The post was successfully added");
                req.session.save(() => res.redirect('/dashboard'));
            }
        });
    }
}

The Post model:

const postSchema = new mongoose.Schema({
    title: {
        type: String,
        required: true
    },
    short_description: {
        type: String,
        required: true
    },
    full_text: {
        type: String,
        required: true
    },
    post_image: {
        type: String,
        required: false
    },
    updated_at: {
        type: Date,
        default: Date.now()
    },
    created_at: {
        type: Date,
        default: Date.now()
    }
});

The error messages are nor rendered in the view, which looks like this:

<div id="messages" class="text-center">
    <% Object.keys(messages).forEach(function (type) { %>
        <% messages[type].forEach(function (message) { %>
            <div class="alert alert-<%= type %>"><%= message %></div>
        <% }) %>
    <% }) %>
</div>

UPDATE:

The index.js file in the root has this code:

const express = require("express");
const dotenv = require("dotenv");
const mongoose = require("mongoose");
const path = require("path");
const morgan = require("morgan");
const bodyParser = require('body-parser');
const cookieParser = require('cookie-parser');
const expressLayouts = require("express-ejs-layouts");
const flash = require("express-flash");
const session = require("express-session");
const app = express();

dotenv.config();

//Conect to MONGODB
mongoose
  .connect(process.env.MONGO_URI, {
    useNewUrlParser: true,
    useUnifiedTopology: true
  })
  .then(() => {
    console.log("conected");
  });

mongoose.connection.on("error", err => {
  console.log(`DB connection error: ${err.message}`);
});

// Set static directory
app.use(express.static(path.join(__dirname, "public")));

// Set views directory
app.set("views", path.join(__dirname, "views"));

// Set view engine
app.set("view engine", "ejs");

// Use Express Layouts
app.use(expressLayouts);

// Morgan Middleware
app.use(morgan("dev"));

// support parsing of application/json type post data
app.use(bodyParser.json());

//support parsing of application/x-www-form-urlencoded post data
app.use(bodyParser.urlencoded({ extended: true }));

app.use(cookieParser());

// Express Sessions Middleware
app.use(session({
  secret: '123',
  resave: true,
  saveUninitialized: true
}));

// Express Messages Middleware
app.use(flash());
app.use(function (req, res, next) {
  res.locals.messages = require('express-messages')(req, res);
  next();
});

// Bring the Dashboard
const dashboardRoute = require("./routes/admin/dashboard");

// Get Dashboard Routes
app.use('/dashboard', dashboardRoute);

What am I doing wrong?

Razvan Zamfir
  • 4,209
  • 6
  • 38
  • 252

8 Answers8

3
exports.addPost = (req, res, next) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
        req.flash('errors', errors.array())
        req.session.save(() => res.redirect('../addpost'));
        //return res.status(400).send(errors.array());
    } else {
            const post = new Post();
                post.title = req.body.title;
                post.short_description = req.body.excerpt
                post.full_text = req.body.body;

            post.save(function(err){
                    if(err){
                            console.log(err);
                            return;
                    } else {
                        req.flash('success', "The post was successfully added");
                        req.session.save(() => res.redirect('/dashboard'));
                    }
            });
    }
}

messages.ejs

<div id="messages" class="text-center">
<% Object.keys(messages).forEach(function (type) { %>
    <% messages[type].forEach(function (message) { %>
        <% if (type === 'errors') {%>
            <div class="alert alert-<%= type %>"><%= message.msg %></div>
        <%} else { %>
            <div class="alert alert-<%= type %>"><%= message %></div>
        <% } %>
    <% }) %>
<% }) %>

I guess this is what you intended to do

  • It worked! can you have a look at **[this question](https://stackoverflow.com/questions/60721476/express-js-application-bug-form-filed-values-do-not-persist)** too? Thanks! – Razvan Zamfir Mar 17 '20 at 12:23
  • I have simplified your version of the `messages.ejs` file (other then that, your solutiom is perfect). Have a look **[here](https://stackoverflow.com/a/60721302/4512005)**. Thanks! – Razvan Zamfir Mar 17 '20 at 14:10
1

You are rendering a template then trying to show flash and then redirect again. Change it to this

req.flash('danger', errors);
req.session.save(() => res.redirect('/dashboard'));

Forget the render... It makes no sense for you to have it there. What render does, it renders and returns a template. Therefore your req.flash and redirect never happens or it happens after the header have already been sent.

res.render() definition:

Renders a view and sends the rendered HTML string to the client. Optional parameters:

locals, an object whose properties define local variables for the view. callback, a callback function. If provided, the method returns both the possible error and rendered string, but does not perform an automated response. When an error occurs, the method invokes next(err) internally.

AND

exports.addPost = (req, res, next) => {
   // Form validation rules
    req.check('title').not().isEmpty().withMessage("The title field is mandatory");
    req.check('body').not().isEmpty().withMessage("The full text field is mandatory");

   const errors = req.validationErrors();
TSlegaitis
  • 1,231
  • 15
  • 29
1

try change your if statement from this:

   if (!errors.isEmpty()) {
        console.log('there are no validation errors');
    } else {
        console.log(errors);
    }
}

to this:

    exports.addPost = (req, res, next) => {
        // Form validation rules
        check('title', '<your error message>')
          .not()
          .isEmpty();
        check('excerpt', '<your error message>')
          .not()
          .isEmpty();
       check('body', '<your error message>')
          .not()
          .isEmpty();

        const errors = validationResult(req);

        const errors = validationResult(req);
        if (!errors.isEmpty()) {
           console.log(errors.array());
        }
}

Edit

If you would like to send an response to your front-end replace the console.log() command into res.send() then parse the answer in your front-end like so:

if (!errors.isEmpty()) {
           return res.send(errors.array());
         // can also send an status and catch it
        // by sending res.status(400).send(errors.array());
        }

Hopefully this makes sense

rerez
  • 111
  • 5
  • Hello! Have you looked at the **[GitHub](https://github.com/Ajax30/XPressBlog)** repo? If not, please do, as it could be relevant for your answer. Thanks! – Razvan Zamfir Mar 10 '20 at 13:00
  • I ger this long error nessage: `{ title: { ValidatorError: Path `title` is required.`.... – Razvan Zamfir Mar 10 '20 at 13:39
  • i have updated my post. Please take another look. Thanks!\ – Razvan Zamfir Mar 10 '20 at 14:21
  • Hi, if you want the errors to be seen in your front-end , you can return the response with the specific error in the array. just replace the 'console.log' into 'return res.send(errors.array())' then parse them in your front-end – rerez Mar 10 '20 at 15:16
  • The template contains and *has to contain* the `messages` variable. The solution does not work. – Razvan Zamfir Mar 10 '20 at 15:56
  • In fact, `return res.send(errors.array());` results in... a blank page. – Razvan Zamfir Mar 10 '20 at 16:14
1

from what i see in the documentation of express-validator you need to provide an array of validation rules(those checks at the top of your controller) when you define the route. It doesn't make much sense for them to be at the top of the request handler since the express-validator won't be able to access the context that provides the request to be validated.

So in the router you need something like this:

router/front-end/posts.js

const validationRules = [// Form validation rules
        check('title', 'The title field id required')
        .not()
        .isEmpty(),
    check('excerpt', 'The excerpt field id required')
        .not()
        .isEmpty(),
 check('body', 'The full text field id required')
        .not()
        .isEmpty()];
// create new post
router.post('/', validationRules, postsController.addPost);

controllers/front-end/posts.js

exports.addPost = (req, res, next) => {

        const errors = validationResult(req);

        if (!errors.isEmpty()) {
            console.log(errors.array());
     }

        if (!errors.isEmpty()) {
            res.render('admin/addpost', {
            layout: 'admin/layout',
             website_name: 'MEAN Blog',
             page_heading: 'Dashboard',
             page_subheading: 'Add New Post',
             errors: errors
            });
            req.flash('danger', errors);
            req.session.save(() => res.redirect('/dashboard'));
        } else {
                const post = new Post();
                    post.title = req.body.title;
                    post.short_description = req.body.excerpt
                    post.full_text = req.body.body;

                post.save(function(err){
                       if(err){
                          console.log(err);
                          return;
                        } else {
                          req.flash('success', "The post was successfully added");
                          req.session.save(() => res.redirect('/dashboard'));
                        }
                });
        }
}

Everything else seem ok, at least from the code you posted.

AndreiC
  • 542
  • 4
  • 16
  • Isn't it more logical to validate the data (form) in the controller? Isn't this "classic"? – Razvan Zamfir Mar 10 '20 at 22:32
  • I'm just wondering how would the `check` method access the request, if no one is sending it as a parameter. Intuition tells me that validation should happen in a middleware, add some property on the request and pass the execution to the next handler(the controller) where the `validationResult (req)` extracts that property created in the validation middleware. – AndreiC Mar 11 '20 at 08:24
  • I don't know. This is why I have asked the question. But I am almost 100% sure that there should not be any logic in the routes file *except* the routes logic. – Razvan Zamfir Mar 11 '20 at 09:25
1

it's not a bug with express-validators, it is the way how validators work in case of middlewares.

At the root level create a directory called utils and inside the directory a validation.js file and add your validation code in it:

utils/validation.js

const { check } = require('express-validator');
exports.addPostCheck = [
    check('title', 'The title field id required')
    .not()
    .isEmpty(),
    check('excerpt', 'The excerpt field id required')
    .not()
    .isEmpty(),
    check('body', 'The full text field id required')
    .not()
    .isEmpty()
];

In the routes/dashboard.js include validation.js

const validator = require('../../utils/validation.js');

Change Line No: 16
From: 
router.post('/post/add', dashboardController.addPost);
To:
router.post('/post/add', validator.addPostCheck, dashboardController.addPost);

In the controllers/admin/dashboard.js

Change Line No: 2
From: 
const { check, validationResult } = require('express-validator');
To:
const { validationResult } = require('express-validator');

Remove Line Nos 29 to 39.

Reference

balajipachai
  • 231
  • 2
  • 6
  • I now have a [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client` in the console and there are no validation error messages. Please have a look at the Code in the **[GitHub repo](https://github.com/Ajax30/XPressBlog)**. – Razvan Zamfir Mar 16 '20 at 15:02
  • That is because of line number 39 in controller/dasboard.js, since the response is returned from line number 33, and again you are trying to return the response that is why you are getting the error `[ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client`. Fix: Remove line number 39. – balajipachai Mar 17 '20 at 08:00
  • Solved the "Cannot set headers" problem. The validation error messages still do not render in the *unsubmitted* form's page, though. – Razvan Zamfir Mar 17 '20 at 08:12
  • For that I think you should consider @TSlegaitis answer above. – balajipachai Mar 17 '20 at 08:15
  • Please give an answer to **[this question](https://stackoverflow.com/questions/60849398/express-js-application-bug-cannot-read-property-transfer-encoding-of-undefine)** too. Thanks! – Razvan Zamfir Mar 28 '20 at 11:40
0

Maybe you should start with this https://express-validator.github.io/docs/ then gradually customise it to fit your need so that you can catch any error along the way.

Awelle
  • 44
  • 4
0

use this code in index.js to get flash error messages locally in EJS ,

app.use(function (req, res, next) {
    res.locals.messages = req.flash();
    next();
});
Adeel Nawaz
  • 398
  • 2
  • 10
  • I have this as messages middleware: `app.use(flash()); app.use(function (req, res, next) { res.locals.messages = require('express-messages')(req, res); next(); });`. What is wrong with it? (It works for the conformation messages). Where shall I put your code in the *contex* of my `index.js` file (see qiestion update)? – Razvan Zamfir Mar 14 '20 at 18:08
  • app.use(flash()) this allow to save messages in express session , yes i have seen in index.js . two options only iterate with errors objects or pass flash messages in locals res.locals.messages = req.flash(); – Adeel Nawaz Mar 14 '20 at 18:16
  • Can you put *your* code in the code I have for `index.js`? (I have tried numerous packages and now I don't know exactly what `index.js` should contain and what not to). Thanks! – Razvan Zamfir Mar 14 '20 at 18:27
  • I should use your code instead of which part of my code? – Razvan Zamfir Mar 16 '20 at 08:18
0

I have applied the solution provided by Saravanakumar T N with a small modification in messages.ejs.

I have: this in the controller:

exports.addPost = (req, res, next) => {
  const errors = validationResult(req);
  const post = new Post();
  if (!errors.isEmpty()) {
    req.flash('danger', errors.array());
    req.session.save(() => res.redirect('../addpost'));
 } else {
    post.title = req.body.title;
    post.short_description = req.body.excerpt
    post.full_text = req.body.body;

    post.save(function(err) {
        if (err) {
            console.log(err);
            return;
        } else {
            req.flash('success', "The post was successfully added");
            req.session.save(() => res.redirect('/dashboard'));
        }
    });
  }
}

In the view:

<div id="messages" class="text-center">
    <% Object.keys(messages).forEach(function (type) { %>
            <% messages[type].forEach(function (message) { %>
                    <div class="alert alert-success <% if (type === 'danger') { %> alert-dismissible <% } %> alert-<%= type %>">
                        <% if (type === 'danger') { %>
                            <button type="button" class="close" data-dismiss="alert">&times;</button>
                            <%= message.msg %>
                        <%} else { %>
                            <%= message %>
                        <% } %>
                </div>
            <% }) %>
    <% }) %>
</div>
Razvan Zamfir
  • 4,209
  • 6
  • 38
  • 252