MongoDB Server-side validation

Server-side validation can be added at the top of any server-side route, as shown below. The validation code returns an errorMessage if it encounters any errors.

router.post(`/cars`, (req, res) => 
{
    // validate input
    const today = new Date();
    if(!/^[a-zA-Z]+$/.test(req.body.model))
    {
        res.json({errorMessage:`Model must be a string`}); 
    }
    else if(!/^[a-zA-Z]+$/.test(req.body.colour))
    {
        res.json({errorMessage:`Colour must be a string`});         
    }
    else // input is valid
    {    
        carsModel.create(req.body, (error, data) => 
        {
            res.json(data)
        })
    }
})

 

If an error occurs, then the errorMessage can be handled in the axios call on the client-side, as shown below. If there is an error, then we deal with it. Otherwise, we proceed as normal.

axios.post(`${SERVER_HOST}/cars`, carObject)
.then(res => 
{   
    if(res.data)
    {
        if (res.data.errorMessage)
        {
            console.log(res.data.errorMessage)    
        }
        else
        {   
            console.log("Record added")
            this.setState({redirectToDisplayAllCars:true})
        } 
    }
    else
    {
        console.log("Record not added")
    }
})

Open the mongoDB project from the previous section in these notes. Change the code so that all database querys are validated in the server-side router code with the following rules:

"Cars" Worked Example

The full project code for the "Cars" Worked Example that is described below can be downloaded from this link.

This code adds server-side validation to all mongoDB interactions.

Client-Side

client/src/components/DisplayAllCars.js

import React, {Component} from "react"
import {Link} from "react-router-dom"

import axios from "axios"

import CarTable from "./CarTable"

import {SERVER_HOST} from "../config/global_constants"


export default class DisplayAllCars extends Component 
{
    constructor(props) 
    {
        super(props)
        
        this.state = {
            cars:[]
        }
    }
    
    
    componentDidMount() 
    {
        axios.get(`${SERVER_HOST}/cars`)
        .then(res => 
        {
            if(res.data)
            {
                if (res.data.errorMessage)
                {
                    console.log(res.data.errorMessage)    
                }
                else
                {           
                    console.log("Records read")   
                    this.setState({cars: res.data}) 
                }   
            }
            else
            {
                console.log("Record not found")
            }
        })
    }

  
    render() 
    {   
        return (           
            <div className="form-container">
                <div className="table-container">
                    <CarTable cars={this.state.cars} /> 

                    <div className="add-new-car">
                        <Link className="blue-button" to={"/AddCar"}>Add New Car</Link>
                    </div>
                </div>
            </div> 
        )
    }
}

For every axios() method that is used throughout the "Cars" example, we shall use the if statement template below:

  1. check to see if res.data is not empty
  2. if res.data is not empty, then it will contain either a custom error message or the valid data from the server-side router that can be used on the client-side
  3. if res.data.errorMessage is not empty, then there is an error for which we have a custom error message
  4. else if res.data.errorMessage is empty, then there is no custom error message, which means that res.data contains the valid data from the server-side router that can be used on the client-side
  5. else if res.data is empty, then there is an error that is not one of our custom errors

        axios.get(`${SERVER_HOST}/cars`)
        .then(res => 
        {
            if(res.data)   //     1. contains either a custom error message or the valid data from the server-side router that can be used on the client-side
            {    
                
                //     2. contains either a custom error message or the valid data from the server-side router that can be used on the client-side

  if (res.data.errorMessage) //     3. contains a custom error message 
  { 

                    ... 

                }
                else  //     4. contains the valid data from the server-side router that can be used on the client-side
                { 

                    ...

                } 
            }
            else   //     5. An error that is not one of our custom errors has occured 
            {
            
                ...

            }
        })

The above template is used in the code below (and in every other of the "Cars" example axios() methods).

    componentDidMount() 
    {
        axios.get(`${SERVER_HOST}/cars`)
        .then(res => 
        {
             if(res.data)
            {
                if (res.data.errorMessage)
                {
                    console.log(res.data.errorMessage)    
                }
                else
                {           
                    console.log("Records read")   
                    this.setState({cars: res.data}) 
                }   
            }
            else
            {
                console.log("Record not found")
            }
        })
    }

The axios.get(`${SERVER_HOST}/cars`) method will get a JSON object that contains all of the cars.
We use the returned JSON object to set the cars state (i.e. this.setState({cars: res.data}))

    componentDidMount() 
    {
        axios.get(`${SERVER_HOST}/cars`)
        .then(res => 
        {
             if(res.data)
            {
                if (res.data.errorMessage)
                {
                    console.log(res.data.errorMessage)    
                }
                else
                {           
                    console.log("Records read")   
                    this.setState({cars: res.data}) 
                }   
            }
            else
            {
                console.log("Record not found")
            }
        })
    }

client/src/components/AddCar.js

import React, {Component} from "react"
import {Redirect, Link} from "react-router-dom"
import Form from "react-bootstrap/Form"

import axios from "axios"

import Button from "../components/Button"

import {SERVER_HOST} from "../config/global_constants"


export default class AddCar extends Component
{
    constructor(props)
    {
        super(props)

        this.state = {
            model:"",
            colour:"",
            year:"",
            price:"",
            redirectToDisplayAllCars:false
        }
    }


    componentDidMount() 
    {     
        this.inputToFocus.focus()        
    }
 
 
    handleChange = (e) => 
    {
        this.setState({[e.target.name]: e.target.value})
    }


    handleSubmit = (e) => 
    {
        e.preventDefault()

        this.setState({ wasSubmittedAtLeastOnce: true });

        const formInputsState = this.validate();
        
        if (Object.keys(formInputsState).every(index => formInputsState[index])) 
        {
            const carObject = {
                model: this.state.model,
                colour: this.state.colour,
                year: this.state.year,
                price: this.state.price,
                wasSubmittedAtLeastOnce: false
            }

            axios.post(`${SERVER_HOST}/cars`, carObject) .then(res => 
            {   
                if(res.data)
                {
                    if (res.data.errorMessage)
                    {
                        console.log(res.data.errorMessage)    
                    }
                    else
                    {   
                        console.log("Record added")
                        this.setState({redirectToDisplayAllCars:true})                    } 
                }
                else
                {
                    console.log("Record not added")
                }
            })
        }
    }


    validateModel()
    {    
        const pattern = /^[A-Za-z]+$/;
        return pattern.test(String(this.state.model))
    }
    
    
    validateColour()
    {    
        const pattern = /^[A-Za-z]+$/;
        return pattern.test(String(this.state.colour))
    }
    
    
    validateYear()
    {    
        const year = parseInt(this.state.year)
        const today = new Date()   
        return (year >= 1990 && year <= today.getFullYear())
    }


    validatePrice()
    {    
        const price = parseInt(this.state.price)
        return (price >= 1000 && price <= 100000)
    }


    validate() 
    {
        return {
            model: this.validateModel(),
            colour: this.validateColour(),
            year: this.validateYear(),
            price: this.validatePrice()
        };
    }


    render()
    { 
        let errorMessage = "";
        if(this.state.wasSubmittedAtLeastOnce)
        {
            errorMessage = <div className="error">Car Details are incorrect<br/></div>;
        }         
    
        return (
            <div className="form-container"> 
                {this.state.redirectToDisplayAllCars ? <Redirect to="/DisplayAllCars"/> : null}                                            
                    
                <Form>
                    {errorMessage}
               
                    <Form.Group controlId="model">
                        <Form.Label>Model</Form.Label>
                        <Form.Control ref = {(input) => { this.inputToFocus = input }} type="text" name="model" value={this.state.model} onChange={this.handleChange} />
                    </Form.Group>
    
                    <Form.Group controlId="colour">
                        <Form.Label>Colour</Form.Label>
                        <Form.Control type="text" name="colour" value={this.state.colour} onChange={this.handleChange} />
                    </Form.Group>
    
                    <Form.Group controlId="year">
                        <Form.Label>Year</Form.Label>
                        <Form.Control type="text" name="year" value={this.state.year} onChange={this.handleChange} />
                    </Form.Group>
    
                    <Form.Group controlId="price">
                        <Form.Label>Price</Form.Label>
                        <Form.Control type="text" name="price" value={this.state.price} onChange={this.handleChange} />
                    </Form.Group> 
            
                    <Button value="Add" className="green-button" onClick={this.handleSubmit}/>            
            
                    <Link className="red-button" to={"/DisplayAllCars"}>Cancel</Link>
                </Form>
            </div>
        )
    }
}

When the user submits the form, Add a car to the database and then set the state to redirect to the DisplayAllCars component.


    handleSubmit = (e) => 
    {

        ...


        axios.post(`${SERVER_HOST}/cars`, carObject)
            .then(res => 
            {   
                if(res.data)
                {
                    if (res.data.errorMessage)
                    {
                        console.log(res.data.errorMessage)    
                    }
                    else
                    {   
                        console.log("Record added")
                        this.setState({redirectToDisplayAllCars:true})
                    } 
                }
                else
                {
                    console.log("Record not added")
                }
            })

        ...

client/src/components/EditCar.js

import React, {Component} from "react"
import Form from "react-bootstrap/Form"
import {Redirect, Link} from "react-router-dom"
import axios from "axios"

import Button from "../components/Button"

import {SERVER_HOST} from "../config/global_constants"

export default class EditCar extends Component 
{
    constructor(props) 
    {
        super(props)

        this.state = {
            model: ``,
            colour: ``,
            year: ``,
            price: ``,
            redirectToDisplayAllCars:false
        }
    }

    componentDidMount() 
    {      
        this.inputToFocus.focus()
  
        axios.get(`${SERVER_HOST}/cars/${this.props.match.params.id}`)
        .then(res => 
        {     
            if(res.data)
            {
                if (res.data.errorMessage)
                {
                    console.log(res.data.errorMessage)    
                }
                else
                { 
                    this.setState({
                        model: res.data.model,
                        colour: res.data.colour,
                        year: res.data.year,
                        price: res.data.price
                    })                }
            }
            else
            {
                console.log(`Record not found`)
            }
        })
    }


    handleChange = (e) => 
    {
        this.setState({[e.target.name]: e.target.value})
    }


    handleSubmit = (e) => 
    {
        e.preventDefault()

        this.setState({ wasSubmittedAtLeastOnce: true });

        const formInputsState = this.validate();
        
        if (Object.keys(formInputsState).every(index => formInputsState[index])) 
        {
            const carObject = {
                model: this.state.model,
                colour: this.state.colour,
                year: this.state.year,
                price: this.state.price
            }

            axios.put(`${SERVER_HOST}/cars/${this.props.match.params.id}`, carObject)
            .then(res => 
            {             
                if(res.data)
                {
                    if (res.data.errorMessage)
                    {
                        console.log(res.data.errorMessage)    
                    }
                    else
                    {      
                        console.log(`Record updated`)
                        this.setState({redirectToDisplayAllCars:true})
                    }
                }
                else
                {
                    console.log(`Record not updated`)
                }
            })
        }
    }


    validateModel()
    {    
        const pattern = /^[A-Za-z]+$/;
        return pattern.test(String(this.state.model))
    }
    
    
    validateColour()
    {    
        const pattern = /^[A-Za-z]+$/;
        return pattern.test(String(this.state.colour))
    }
    
    
    validateYear()
    {    
        const year = parseInt(this.state.year)
        const today = new Date()   
        return (year >= 1990 && year <= today.getFullYear())
    }


    validatePrice()
    {    
        const price = parseInt(this.state.price)
        return (price >= 1000 && price <= 100000)
    }


    validate() 
    {
        return {
            model: this.validateModel(),
            colour: this.validateColour(),
            year: this.validateYear(),
            price: this.validatePrice()
        };
    }
    
    
    render() 
    {
        let errorMessage = "";
        if(this.state.wasSubmittedAtLeastOnce)
        {
            errorMessage = <div className="error">Car Details are incorrect<br/></div>;
        }  
        return (
            <div className="form-container">
    
                {this.state.redirectToDisplayAllCars ? <Redirect to="/DisplayAllCars"/> : null}  
                        
                <Form>
                
                    {errorMessage}
                
                    <Form.Group controlId="model">
                        <Form.Label>Model</Form.Label>
                        <Form.Control ref = {(input) => { this.inputToFocus = input }} type="text" name="model" value={this.state.model} onChange={this.handleChange} />
                    </Form.Group>

                    <Form.Group controlId="colour">
                        <Form.Label>Colour</Form.Label>
                        <Form.Control type="text" name="colour" value={this.state.colour} onChange={this.handleChange} />
                    </Form.Group>

                    <Form.Group controlId="year">
                        <Form.Label>Year</Form.Label>
                        <Form.Control type="text" name="year" value={this.state.year} onChange={this.handleChange} />
                    </Form.Group>
        
                    <Form.Group controlId="price">
                        <Form.Label>Price</Form.Label>
                        <Form.Control type="text" name="price" value={this.state.price} onChange={this.handleChange} />
                    </Form.Group>
  
                    <Button value="Update" className="green-button" onClick={this.handleSubmit}/>  
    
                    <Link className="red-button" to={"/DisplayAllCars"}>Cancel</Link>
                </Form>
            </div>
        )
    }
}

When the component mounts, the current car's ID will be passed to the router. The details of the car that matches the current car's ID will be returned from the server-side router and used to set this component's state.

    componentDidMount() 
    {      
        this.inputToFocus.focus()
  
        axios.get(`${SERVER_HOST}/cars/${this.props.match.params.id}`)
        .then(res => 
        {     
            if(res.data)
            {
                if (res.data.errorMessage)
                {
                    console.log(res.data.errorMessage)    
                }
                else
                {
					 this.setState({
                        model: res.data.model,
                        colour: res.data.colour,
                        year: res.data.year,
                        price: res.data.price
                    })
                }
            }
            else
            {
                console.log(`Record not found`)
            }
        })
    }

When the form is submitted, the carObject is sent to the router, where it will overwrite the document with the matching ID.

The axios.put() method has two parameters:


    handleSubmit = (e) => 
    { 

        ...             

            axios.put(`${SERVER_HOST}/cars/${this.props.match.params.id}`, carObject)
            .then(res => 
            {             
                if(res.data)
                {
                    if (res.data.errorMessage)
                    {
                        console.log(res.data.errorMessage)    
                    }
                    else
                    {      
                        console.log(`Record updated`) this.setState({redirectToDisplayAllCars:true})
                    }
                }
                else
                {
                    console.log(`Record not updated`)
                }
            })

        ...

Server-Side

server/routes/cars.js

const router = require(`express`).Router()

const carsModel = require(`../models/cars`)

// read all records
router.get(`/cars`, (req, res) => 
{   
    //user does not have to be logged in to see car details
    carsModel.find((error, data) => 
    {
        res.json(data)
    })
})


// Read one record
router.get(`/cars/:id`, (req, res) => {
    carsModel.findById(req.params.id, (error, data) => 
    {
        res.json(data)
    })
})


// Add new record
router.post(`/cars`, (req, res) => 
{
    // validate input
    const today = new Date();
    if(!/^[a-zA-Z]+$/.test(req.body.model))
    {
        res.json({errorMessage:`Model must be a string`}); 
    }
    else if(!/^[a-zA-Z]+$/.test(req.body.colour))
    {
        res.json({errorMessage:`Colour must be a string`});         
    }
    else if(req.body.year < 1990)     // between 1990 and the current year
    {
        res.json({errorMessage:`Year needs to be greater than or equal to 1990`});         
    }
    else if(req.body.year > today.getFullYear())
    {
        res.json({errorMessage:`Year needs to be this year or less`});         
    }
    else if(req.body.price < 1000 || req.body.price > 100000)       // between €1000 and €100000                
    {
        res.json({errorMessage:`Price needs to be between €1000 and €100000`}); 
    }
    else // input is valid
    {    
        carsModel.create(req.body, (error, data) => 
        {
            res.json(data)
        })
    }
})


// Update one record
router.put(`/cars/:id`, (req, res) => 
{
    // validate input
    const today = new Date();
    if(!/^[a-zA-Z]+$/.test(req.body.model))
    {
        res.json({errorMessage:`Model must be a string`}); 
    }
    else if(!/^[a-zA-Z]+$/.test(req.body.colour))
    {
        res.json({errorMessage:`Colour must be a string`});         
    }
    else if(req.body.year < 1990)     // between 1990 and the current year
    {
        res.json({errorMessage:`Year needs to be greater than or equal to 1990`});         
    }
    else if(req.body.year > today.getFullYear())
    {
        res.json({errorMessage:`Year needs to be this year or less`});         
    }
    else if(req.body.price < 1000 || req.body.price > 100000)       // between €1000 and €100000                
    {
        res.json({errorMessage:`Price needs to be between €1000 and €100000`}); 
    }
    else // input is valid
    {
        carsModel.findByIdAndUpdate(req.params.id, {$set: req.body}, (error, data) => 
        {
            res.json(data)
        })        
    }
})


// Delete one record
router.delete(`/cars/:id`, (req, res) => 
{
    carsModel.findByIdAndRemove(req.params.id, (error, data) => 
    {
        res.json(data)
    })       
})

module.exports = router

Appropriate validation is done at the start of each route.

If the input is invalid, then a custom errorMessage will be returned to the client-side axios() method that called this router.post() method.. The client-side axios() method that called this method will be able to access the returned errorMessage as res.data.errorMessage

If the data passes all of the validation tests, then it is valid. In this case, the route behaves as normal. It interacts with the database and returns some data to the client-side axios() method that called it.

// Add new record
router.post(`/cars`, (req, res) => 
{
    // validate input
    const today = new Date();
    if(!/^[a-zA-Z]+$/.test(req.body.model))
    {
        res.json({errorMessage:`Model must be a string`}); 
    }
    else if(!/^[a-zA-Z]+$/.test(req.body.colour))
    {
        res.json({errorMessage:`Colour must be a string`});         
    }
    else if(req.body.year < 1990)     // between 1990 and the current year
    {
        res.json({errorMessage:`Year needs to be greater than or equal to 1990`});         
    }
    else if(req.body.year > today.getFullYear())
    {
        res.json({errorMessage:`Year needs to be this year or less`});         
    }
    else if(req.body.price < 1000 || req.body.price > 100000)       // between €1000 and €100000                
    {
        res.json({errorMessage:`Price needs to be between €1000 and €100000`}); 
    }
    else // input is valid
    {    
        carsModel.create(req.body, (error, data) => 
        {
            res.json(data)
        })
    }
})
Every route has the same validation structure.

The validation code above does not do any client-side validation. Adjust the client-side code to include validation.

Add to the code at this link, so as to also validate a user's login on the server side. The user should only be given a generic message "User name or password is invalid" rather than a detailed description of the reason for the failed login.

 
<div align="center"><a href="../versionC/index.html" title="DKIT Lecture notes homepage for Derek O&#39; Reilly, Dundalk Institute of Technology (DKIT), Dundalk, County Louth, Ireland. Copyright Derek O&#39; Reilly, DKIT." target="_parent" style='font-size:0;color:white;background-color:white'>&nbsp;</a></div>