Copyright Derek O'Reilly, Dundalk Institute of Technology (DkIT), Dundalk, Co. Louth, Ireland.
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:
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.
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:
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") } }) }
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") } }) ...
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`) } }) ...
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.
Copyright Derek O' Reilly, Dundalk Institute of Technology (DkIT), Dundalk, Co. Louth, Ireland.