We know that mongoose provides us an easy way to do validation. But suppose you are using express+mongoose to building a microservice; and some clients (could be web-app, mobile app etc.) needs to consume it.
Usually, I prefer to response JSON back with simple error code and message. In most cases, the clients who can create their own messages depending on which language they are showing to users.
By default, if we catch the error from mongoose, we can get JSON response such as:
JSON Response
{
"errors": {
"price": {
"message": "Path `price` (-1) is less than minimum allowed value (0).",
"name": "ValidatorError",
"properties": {
"min": 0,
"type": "min",
"message": "Path `{PATH}` ({VALUE}) is less than minimum allowed value (0).",
"path": "price",
"value": -1
},
"kind": "min",
"path": "price",
"value": -1,
"$isValidatorError": true
},
"code": {
"message": "Product with given code already exists",
"name": "ValidatorError",
"properties": {
"type": "user defined",
"message": "Product with given code already exists",
"path": "code",
"value": "p-1000"
},
"kind": "user defined",
"path": "code",
"value": "p-1000",
"$isValidatorError": true
}
},
"_message": "Product validation failed",
"message": "Product validation failed: price: Path `price` (-1) is less than minimum allowed value (0)., code: Product with given code already exists",
"name": "ValidationError"
}
Restful Api Controller
exports.createOne = async(function* list(req, res) {
try {
const product = new Product(req.body)
const newProduct = yield product.save()
res.json(newProduct)
} catch (err) {
res.status(400).json(err)
}
})
Model Product.js
const mongoose = require('mongoose')
const Schama = mongoose.Schema
const minlength = [5, 'The value of `{PATH}` (`{VALUE}`) is shorter than the minimum allowed length ({MINLENGTH}).'];
const ProductSchema = new Schama({
code: { type: String, required: true, minlength, index: true, unique: true, trim: true, lowercase: true },
name: { type: String, required: true, trim: true },
price: { type: Number, required: true, min: 0, max: 100000 },
categories: [String],
})
ProductSchema.path('code').validate(function uniqueEmail(code, fn) {
const Product = mongoose.model('Product')
// Check only when it is a new Product or when code field is modified
if (this.isNew || this.isModified('code')) {
Product.find({ code }).exec((err, products) => {
fn(!err && products.length === 0)
})
} else fn(true)
}, 'Product with given code already exists')
ProductSchema.statics = {
/**
* List products
*
* #param {Object} options
* #api private
*/
pageList: function pageList(conditions, index, size) {
const criteria = conditions || {}
const page = index || 0
const limit = size || 30
return this.find(criteria)
.populate('user', 'name username')
.sort({ createdAt: -1 })
.limit(limit)
.skip(limit * page)
.exec()
},
}
mongoose.model('Product', ProductSchema)
What I expect
I am trying to wrap the error message to make it simple to consumer.
It could be like:
{
"errors": [
{
"message": "Path `price` (-1) is less than minimum allowed value (0).",
"code": "100020"
},
{
"message": "Product with given code already exists",
"code": "100021"
}
],
"success": false
}
The code and the corresponding message will be maintained on api documents. The message is usualy useful for consumer to understand the code and consumer (such as web client) could create their own message such as French messages according to the code and show to end users.
How can I leverage mongoose's valiation to accomplish this? Maybe I could loop erros's properties and combine an code using ${path}-${kind}.
I know that in most case, Client side should do the validation before calling apis. But there must be some cases that errors have to be thrown by APIs.
Any idea for this?
Related
price: {
type: Number,
required: [true, 'A tour must have a price'],
},
priceDiscount: {
type: Number,
validate: function (val) {
return val < this.price;
}
The validation here tests if the discount price is less than the actual price if so it should work with no problems (it works if I am creating a new tour on the update it doesn't)
It just gives back a validation error even if the discount is less than the price ( "price": 997,
"priceDiscount":10)
"status": "FAIL",
"message": {
"errors": {
"priceDiscount": {
"name": "ValidatorError",
"message": "Validator failed for path `priceDiscount` with value `10`",
"properties": {
"message": "Validator failed for path `priceDiscount` with value `10`",
"type": "user defined",
"path": "priceDiscount",
"value": 10
},
"kind": "user defined",
"path": "priceDiscount",
"value": 10
}
},
"_message": "Validation failed",
"name": "ValidationError",
"message": "Validation failed: priceDiscount: Validator failed for path `priceDiscount` with value `10`"
}
I already have my runValidators: true
exports.UpdateTour = async (req, res) => {
try {
const upTour = await Tour.findByIdAndUpdate(req.params.id, req.body, {
new: true,
runValidators: true,
});
res.status(200).json({
status: 'success',
data: {
tour: `UPDATE TOUR #${req.params.id}
${upTour}`,
},
});
} catch (err) {
res.status(400).json({
status: 'FAIL',
message: err,
});
}
};
I found a solution that might not be optimal but it worked. I used a middleware function that checks the condition whenever the findByIdAndUpdate is triggered and it solved the problem.
Mongoose middleware docs where I came up with this idea
code sample:
tourSchema.post(/Update$/, async function (docs, next) {
let tour = await this.model.findOne(this.getQuery());
if (tour.price <= tour.priceDiscount) {
//set the discount price to 0 if the price is lower
tour.set({ priceDiscount: 0 });
tour.save();
next(new Error('The discount price is larger than the price'));
} else {
next();
}
});
I have set up Mongoose custom validation with errors and would like to display these error messages in React. I am unfortunately unable to retrieve the error messages. I have tried looking for solutions, but am unfortunately still having trouble.
My code is as follows:
Server-side:
- dataModel.js
const mongoose = require("mongoose");
const uniqueValidator = require("mongoose-unique-validator");
const moment = require("moment");
const dataSchema = mongoose.Schema(
{
name: {
type: String,
required: [true, "Name is required."],
validate: {
validator: function (name) {
return /^[a-zA-Z]+$/.test(name);
},
message: "Only alphabetic characters allowed.",
},
},
surname: {
type: String,
required: [true, "Surname is required."],
validate: {
validator: function (surname) {
return /^[a-zA-Z]+$/.test(surname);
},
message: "Only alphabetic characters allowed.",
},
},
idNumber: {
type: String,
required: [true, "ID Number is required."],
unique: true,
validate: [
{
validator: function (idNumber) {
return idNumber.toString().length === 13;
},
message: (idNumber) =>
`ID Number Must Have 13 Numbers. You entered ${
idNumber.value
}, which is ${idNumber.value.toString().length} numbers long.`,
},
{
validator: function (idNumber) {
return !isNaN(parseFloat(idNumber)) && isFinite(idNumber);
},
message: (idNumber) =>
`ID Number Can Only Contain Number Values. You entered ${idNumber.value}.`,
},
],
},
dateOfBirth: {
type: String,
required: [true, "Date of Birth is required."],
validate: {
validator: function (dateOfBirth) {
return moment(dateOfBirth, "DD/MM/YYYY", true).isValid();
},
message: "Invalid Date of Birth Format. Expected DD/MM/YYYY.",
},
},
},
{
timestamps: true,
}
);
dataSchema.plugin(uniqueValidator, { message: "ID Number Already Exists." });
module.exports = mongoose.model("Data", dataSchema);
- dataController.js
exports.addController = async (req, res) => {
const { firstName, surname, idNumber, dateOfBirth } = req.body;
const newData = new Data({
name: firstName,
surname,
idNumber,
dateOfBirth,
});
try {
await newData.save();
res.send({ message: "Data Added Successfully" });
} catch (error) {
if (error.name === "ValidationError") {
let errors = {};
Object.keys(error.errors).forEach((key) => {
errors[key] = error.errors[key].message;
});
console.log(errors)
return res.status(400).send(errors);
}
res.status(500).send("Something went wrong");
}
};
Output - console.log:
Client-side:
- dataForm.js
const addData = async () => {
try {
axios({
url: "/data/add",
method: "post",
data: {
firstName,
surname,
idNumber,
dateOfBirth,
},
headers: {
"Content-type": "application/json",
},
}).then(function (response) {
alert(response.data.message);
console.log(response.data.message);
});
} catch (error) {
console.log(error);
}
};
Output - Console:
Output - Postman (Initial):
{
"message": [
"Only alphabetic characters allowed.",
"ID Number Can Only Contain Number Values. You entered 888888888888a."
],
"error": {
"errors": {
"surname": {
"name": "ValidatorError",
"message": "Only alphabetic characters allowed.",
"properties": {
"message": "Only alphabetic characters allowed.",
"type": "user defined",
"path": "surname",
"value": "Bösiger"
},
"kind": "user defined",
"path": "surname",
"value": "Bösiger"
},
"idNumber": {
"name": "ValidatorError",
"message": "ID Number Can Only Contain Number Values. You entered 888888888888a.",
"properties": {
"message": "ID Number Can Only Contain Number Values. You entered 888888888888a.",
"type": "user defined",
"path": "idNumber",
"value": "888888888888a"
},
"kind": "user defined",
"path": "idNumber",
"value": "888888888888a"
}
},
"_message": "Data validation failed",
"name": "ValidationError",
"message": "Data validation failed: surname: Only alphabetic characters allowed., idNumber: ID Number Can Only Contain Number Values. You entered 888888888888a."
}
}
Output - Postman (Current):
I would appreciate any help that anyone is willing to offer.
I have managed to sort the problem out and return and display the Mongoose validation errors on the React frontend.
I amended the React post method as follows:
const addData = async () => {
try {
let response = await axios({
url: "http://localhost:8080/data/add",
method: "post",
data: {
firstName,
surname,
idNumber,
dateOfBirth,
},
headers: {
"Content-type": "application/json",
},
})
.then((response) => {
alert(response.data.message);
})
.then(() => {
window.location.reload();
});
alert(response.data.message);
} catch (error) {
alert(Object.values(error.response.data) + ".");
}
};
I had to format the method as the error code was not being reached and had to return and display the data using Object.values() as the responses were objects.
Thank you #cmgchess for pointing me in the right direction.
I'm a beginner who starts to learn node.js. In the middle of the way, I got a problem with a notification like this
{
"errors": {
"password": {
"name": "ValidatorError",
"message": "Path `password` is required.",
"properties": {
"message": "Path `password` is required.",
"type": "required",
"path": "password"
},
"kind": "required",
"path": "password"
},
"email": {
"name": "ValidatorError",
"message": "Path `email` is required.",
"properties": {
"message": "Path `email` is required.",
"type": "required",
"path": "email"
},
"kind": "required",
"path": "email"
},
"name": {
"name": "ValidatorError",
"message": "Path `name` is required.",
"properties": {
"message": "Path `name` is required.",
"type": "required",
"path": "name"
},
"kind": "required",
"path": "name"
}
},
"_message": "User validation failed",
"message": "User validation failed: password: Path `password` is required., email: Path `email` is required., name: Path `name` is required."}
This My User Model
const mongoose = require('mongoose')
const validator = require('validator')
const User = mongoose.model('User', {
name: {
type: String,
required: true,
trim: true
},
email: {
type: String,
required: true,
trim: true,
lowercase: true,
validate(value) {
if (!validator.isEmail(value)) {
throw new Error('Email is invalid')
}
}
},
password:{
type: String,
required: true,
minlength: 7,
trim: true,
validate(value){
if (value.toLowerCase().includes('password')) {
throw new Error('Password cannot contain "password"')
}
}
},
age: {
type: Number,
default: 0,
validate(value) {
if (value < 0 ){
throw new Error('Age must be a positive number')
}
}
}
})
module.exports = User
With this user route
const express = require('express')
require('./db/mongoose')
const User = require('./models/user')
const Task = require('./models/task')
const app = express()
const port = process.env.PORT || 3000
app.use(express.json())
app.post('/users', async (req, res) => {
const user = new User(req.body)
try{
await user.save()
res.status(201).send(user)
} catch (e) {
res.status(400).send(e)
}
})
app.listen(port, () => {
console.log('Server is up on port' +port)
})
Does anyone understand why it would be happening?
Hope I get the answer in this forum to continue my study. Thank you in advance for your help guys. Really appreciate it.
Try this,
In my case I used this
app.use(express.json())
below
app.use("/users",usersController);
So, this cause the problem as my data is not parsing properly.
const express = require("express");
const app = express();
const connect = require("./config/db");
const usersController = require("./controller/user.controller");
app.use(express.json());
app.use("/users",usersController);
const start = async () => {
await connect();
app.listen(2000, async function () {
console.log("listening on port 2000");
});
};
module.exports = start;
As you have defined all these fields as required:true in your schema that's why its happening, you have to provide values to these fields i.e. while testing your api do fill these fields with some value in body.
In your "Postman" before sending, change to "body" and then to "raw" module and at the end where "Text" is, change to "JSON" so your code will work.
I can add a new item to the database if I get a correctly formatted JSON file in the body where every required field contains something. If its false, right now I just return a JSON file like this:
{
"succes": false
}
But I also want to return an error message. I have already implemented the error string in the Model but I dont know how can I pull this out, if the catch block catches the error...
My add new item method:
exports.addBootcamp = async (req, res, next) => {
try {
const bootcamp = await Bootcamp.create(req.body);
if (!bootcamp) {
return res.status(404).json({ succes: false });
}
res.status(201).json({
succes: true,
data: bootcamp
});
} catch (err) {
return res.status(404).json({ succes: false });
}
};
The beggining part of my Model:
const BootcampShema = new mongoose.Schema({
name: {
type: String,
required: [true, 'Please add a name'], //first error message
unique: true,
trim: true,
maxlength: [50, 'Name cannot be more than 50 characters']
},
slug: String,
description: {
type: String,
required: [true, 'Please add a description'], //second error message
maxlength: [500, 'Description cannot be more than 500 characters']
},
//...etc
Of course these are in seperate js files but I can export them.
In this case we'll get a ValidationError from database which will be encapsulated in error object.
Modify your catch statement to below:
try {
// as it is
}
catch (err) {
return res.status(404).json({
succes: false,
message: err.message
});
}
Mongo db return the error object as below. From this structure you can extract whatever info you want and return that to user.
{
"errors": {
"name": {
"message": "Please add a name",
"name": "ValidatorError",
"properties": {
"message": "Please add a name",
"type": "required",
"path": "name"
},
"kind": "required",
"path": "name"
}
},
"_message": "Name validation failed",
"message": "Name validation failed: camera_name: Please add a name",
"name": "ValidationError"
}
Here Please add a name is the same text we entered in our model.
I'm using Sequelize as an ORM for my project. I have this structure:
const Event = sequelize.define('event', {
// fields defined
});
const Question = sequelize.define('question', {
description: {
type: Sequelize.STRING,
allowNull: false,
defaultValue: '',
validate: {
notEmpty: { msg: 'Description should be set.' }
},
},
// other fields defined
});
Event.hasMany(Question);
Question.belongsTo(Event);
Then I create an instance of the Event model, with associate, like that:
const body = {
questions: [
{ description: '' } // is obviously invalid
],
// some other fields
}
const newEvent = await Event.create(body, {
include: [ Question ]
});
If I have validation errors for the Event instance itself, it returns SequelizeValidationError where I can see the path attribute for each ValidationErrorItem. However, when I have the validation error on a child model, the path attribute for this validation error is unclear:
{
"message": "Description should be set.",
"type": "Validation error",
"path": "description",
"value": "",
"origin": "FUNCTION",
"instance": {
"required": true,
"id": null,
"description": "",
"event_id": 60,
"updated_at": "2018-06-11T12:25:04.666Z",
"created_at": "2018-06-11T12:25:04.666Z"
},
"validatorKey": "notEmpty",
"validatorName": "notEmpty",
"validatorArgs": [
{
"msg": "Description should be set."
}
],
"__raw": {
"validatorName": "notEmpty",
"validatorArgs": [
{
"msg": "Description should be set."
}
]
}
The problem is, it's unclear what caused this error and which child is invalid. When I've used Mongoose as an ORM, if I'd do the same, the path attribute would be equal to something like questions.0.description, and that is way more clear, that way you can see which child is invalid.
So, my question is: is there a way to set up the path attribute while validating the child models?
Apparently it's not presented yet, I've filed an issue on Sequelize repo, here it is: https://github.com/sequelize/sequelize/issues/9524