Server-Side Routing

The full project code for the example code listed on this webpage can be downloaded from this link.

A RESTful (Representational State Transfer) API associates a HTTP verb (getpost, put, delete, patch) to a URI path on the server-side and to a function that is called to handle the server-side URI path.

A URI (Uniform Resource Identifier) is a string that identifies a physical or logical resource. A URI looks similar to a URL. In fact, a URL is a sub-set of URI.

HTTP verb CRUD Action URI Path Examples
get read returns requested data

/products/

returns the details of all products
     

/products/123

returns the details of product 123
post create creates a new record /products/corrolla/red/2020/25000 creates a product with model=corolla, colour=red, year=2020 and price=25000
put update updates an entire existing record /products/123
updates product 123 with the contents of the body (also called the payload) that is passed to the server with the api call
      /products/123/corrolla/red/2020/20000 updates product 123 so that model=corolla, colour=red, year=2020 and price=20000
patch update updates part of an existing record /products/123 updates product 123 with the contents of the body (also called the payload) that is passed to the server with the api call
      /products/price/123/20000 updates the price of product 123 with the value 20000
delete delete deletes an existing record /products/123 deletes product 123

The RESTful API data communication is independent of the development language being used in either the requester or the provider of the RESTful service.

 

Within a Node.js web application, Express routing implements the RESTful API to allow access to data sources that are stored on the server-side.

In Express, all routes have the following layout

router.HTTP_verb(URI_path, middlewear_function(req, res))

router
This is the server-side router. The router will be declared at the top of the file that contains the server-side routes.
const router = require(`express`).Router()
HTTP_verb
This is the RESTful API verb:
URI_path
This is the URI path that the route handles
middlewear_function(req, res, next)
Middleware functions have access to:

Axios

We use Axios on the client-side to communicate with server-side Express routes.

We shall store the server-side URL in a .env variable called SERVER_HOST.

Other than the SERVER_HOST, the client-side and server-side URI paths must match, as shown below:

// client-side
axios.get(`${SERVER_HOST}/products/${id}`)
.then(res => 
{
    ...
})






// server-side
router.get(`/products/:id`, (req, res) => 
{
    ...
})

Each HTTP_verb has a client-side Axios method and a server-side route associated with it.

get
On the client-side, the JSON object that is returned from an Axios method is stored in res.data
If res.data is empty, it means that no JSON data was returned.
// client-side
axios.get(`${SERVER_HOST}/products/${id}`)
.then(res => 
{
    if(res.data)
    {          
        console.log("Record read") 

        // we can now use res.data on the client-side   

    }
    else // res.data is empty
    {
        console.log("Record not found")
    }
})







// server-side
router.get(`/products/:id`, (req, res) => 
{
    console.log(req.params.id)

    // we can use req.params.id on the server side
})
post
// client-side
axios.post(`${SERVER_HOST}/products/add/${model}/${colour}/${year}/${price}`)
.then(res => 
{
    if(!res.data)
    {          
        console.log("Record not added")
    }
})






// server-side
router.post(`/products/add/:model/:colour/:year/:price`, (req, res) => 
{
    console.log(req.params.model)    // req.params will hold the model, colour, year and price
})
Axios allows us to pass the post data as a single object. In the example below, newProductJSON would hold the model, colour , year and price.
// client-side
axios.post(`${SERVER_HOST}/products/add`, newProductJSON)
.then(res => 
{
    if(!res.data)
    {          
        console.log("Record not added")
    }
})






// server-side
// On the server-side, the newProductJSON will be held in req.body
router.post(`/products/add`, (req, res) => 
{
    console.log(req.body)    // req.body holds the properties from newProductJSON
})
put
We us put when we want to modify all of the properties of a server-side resource. Axios allows us to pass the put data as a single object. In the example below, updatedProductJSON would hold the model, colour , year and price.
// client-side
axios.put(`${SERVER_HOST}/products/${id}`, updatedProductJSON)
.then(res => 
{
    if(!res.data)
    {          
        console.log("Record not modified")
    }
})






// server-side
router.put(`/products/:id`, (req, res) => 
{
    console.log(req.params.id)
    console.log(req.body.model)   // req.body holds the properties from updatedProductJSON
})
patch
We us patch when we only want to modify some (but not all) of the properties of a server-side resource. In the example below, we are only modifying the price.
// client-side
axios.patch(`${SERVER_HOST}/products/${id}`, {price: newPrice})
.then(res => 
{
    if(!res.data)
    {          
        console.log("Record not modified")
    }
})






// server-side
router.patch(`/products/:id`, (req, res) => 
{
    console.log(req.params.id)
    console.log(req.body.price)  // req.body holds the value of the JSON object {price: newPrice}
})
delete
// client-side
axios.delete(`${SERVER_HOST}/products/${id}`)
.then(res => 
{
    if(!res.data)
    {          
        console.log("Record not deleted")
    }
})






// server-side
router.delete(`/products/:id`, (req, res) => 
{
    console.log(req.params.id)
})

"Cars" Worked Example

The full project code for the example code listed on this webpage can be downloaded from this link.

In the previous example, we stored the cars.json data on the client side. We shall now store the cars data on the server-side and access it using routing and the RESTful API. We shall store the car data in a JSON object.

We shall use Axios methods on the client-side to access the server-side routes. The server-side routes will access the cars JSON object and return the relavent data to the client-side.

The server-side routing code will access a JSON object. We shall need code to do each of the following four tasks on the JSON object: (1)get all items as a JSON, (2)get one item as a JSON, (3)modify one item based on a JSON that contains the new details and (4)delete one item from the JSON. Given the each car item has five properties (id, model, colour, year and price), write test code to implement each of the four identified tasks, as shown here.

Client-Side

client/src/config/global_constants.js

// This file holds global constants that are visible on the Client-side


// Server
export const SERVER_HOST = `http://localhost:4000`

We need access to the server from the client-side Axios methods. We shall store the server name in SERVER_HOST.

client/src/components/LinkInClass.js

//Author: Derek O Reilly
//
// Helper Component class that allows us to have a button that renders the same way as a <Link> component
// Use this class to link to functions within the same class
// Use <Link> to link to Components that are in other routes
import React, {Component} from "react"


export default class LinkInClass extends Component
{
    render()
    {
        return (
            <span tabIndex="0" className={this.props.className} onClick={(event) => {this.props.onClick(event)}}>     
                {this.props.value}
            </span>
        )
    }
}

LinkInClass will allow us to have links that point to a method within a class, but that have the same look-and-feel as a <Link>. In this example, we use it to make all of the buttons look the same.

client/src/components/CarTable.js

import React, {Component} from "react"
import CarTableRow from "./CarTableRow"


export default class CarTable extends Component 
{
    render() 
    {
        return (
            <table>
                <thead>
                    <tr>
                        <th>Model</th>
                        <th>Colour</th>
                        <th>Year</th>
                        <th>Price</th>
                        <th> </th>
                    </tr>
                </thead>
                  
                <tbody>
                    {this.props.cars.map((car) => <CarTableRow key={car._id} car={car}/>)}
                </tbody>
            </table>      
        )
    }
}

We shall add an additional column, which will have an EDIT and DELETE button for each car in the table. The empty table header cell in the code below accounts for this in the table header.

                        <th> </th>

client/src/components/CarTableRow.js

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


export default class CarTableRow extends Component 
{    
    render() 
    {
        return (
            <tr>
                <td>{this.props.car.model}</td>
                <td>{this.props.car.colour}</td>
                <td>{this.props.car.year}</td>
                <td>{this.props.car.price}</td>
                <td>
                    <Link className="green-button" to={"/EditCar/" + this.props.car._id}>Edit</Link>                    
                    <Link className="red-button" to={"/DeleteCar/" + this.props.car._id}>Delete</Link>   
                </td>
            </tr>
        )
    }
}

The code below includes an EDIT and DELETE button for each car in the table.

                <td>
                    <Link className="green-button" to={"/EditCar/" + this.props.car._id}>Edit</Link>                    
                    <Link className="red-button" to={"/DeleteCar/" + this.props.car._id}>Delete</Link>   
                </td>

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 LinkInClass from "../components/LinkInClass"

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()

        const carObject = {
            model: this.state.model,
            colour: this.state.colour,
            year: this.state.year,
            price: this.state.price
        }        

        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")
            }
        })
    }


    render()
    { 
        return (
            <div className="form-container"> 
                {this.state.redirectToDisplayAllCars ? <Redirect to="/DisplayAllCars"/> : null}                                            
                    
                <Form>               
                    <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> 
            
                    <LinkInClass value="Add" className="green-button" onClick={this.handleSubmit}/>            
            
                    <Link className="red-button" to={"/DisplayAllCars"}>Cancel</Link>
                </Form>
            </div>
        )
    }
}

In order to use Axios in a file, we need to import the axios library.

import axios from "axios"

In all of our "Cars" examples, there will always be three possible responses that a server-side route can give to any of the Axios methods:

res.data can contain an errorMessage
In this example, on the server-side we can set an errorMessage as part of the data that will be returned to the Axios method. This allows us to have custom error messages.
res.data can contain data
This is the data that is returned by the server-side route.
res.data can be empty
If res.data is empty, then it is an error.
        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")
            }
        })

The redirectToDisplayAllCars flag is used to control the exit from this Component and the return to the main DisplayAllCars Component upon successful addition of a new car. The flag is set to false in the constructor. The same redirectToDisplayAllCars redirect flag logic will be used in several different components throughout this code.

    constructor(props)
    {
        super(props)

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

At the beginning of the render() method the flag is checked. If it is true, then the code redirects to the DisplayAllCars Component.

    render()
    { 
        return (
            <div className="form-container"> 
                {this.state.redirectToDisplayAllCars ? <Redirect to="/DisplayAllCars"/> : null}    

                ...

The flag will be set to true after the Axios method receives a success indicator from the server-side router (i.e. if the res.data from the server-side router is not empty and does not contain an error message).

        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 LinkInClass from "../components/LinkInClass"

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()

        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`)
            }
        })
    }

    
    render() 
    {  
        return (
            <div className="form-container">
    
                {this.state.redirectToDisplayAllCars ? <Redirect to="/DisplayAllCars"/> : null}  
                        
                <Form>
                    <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>
  
                    <LinkInClass value="Update" className="green-button" onClick={this.handleSubmit}/>  
    
                    <Link className="red-button" to={"/DisplayAllCars"}>Cancel</Link>
                </Form>
            </div>
        )
    }

}

When the component mounts, we get the car details from the server via an axios.get() method. This will place the details into the 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 user submits the form, the modified data is sent to the server via an axios.push() method.

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

        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`)
            }
        })
    }

The redirectToDisplayAllCars redirect flag logic is described in the AddCar.js code previous on this webpage.

client/src/components/DeleteCar.js

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

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


export default class DeleteCar extends Component 
{
    constructor(props) 
    {
        super(props)
        
        this.state = {
            redirectToDisplayAllCars:false
        }
    }
    
    
    componentDidMount() 
    {   
        axios.delete(`${SERVER_HOST}/cars/${this.props.match.params.id}`)
        .then(res => 
        {
            if(res.data)
            {
                if (res.data.errorMessage)
                {
                    console.log(res.data.errorMessage)    
                }
                else // success
                { 
                    console.log("Record deleted")
                }
                this.setState({redirectToDisplayAllCars:true})
            }
            else 
            {
                console.log("Record not deleted")
            }
        })
    }
  
  
    render() 
    {
        return (
            <div>   
                {this.state.redirectToDisplayAllCars ? <Redirect to="/DisplayAllCars"/> : null}                      
            </div>
        )
    }
}

When the component mounts, the axios.delete() method is used to send theid to the server, so that this data can be deleted. The redirectToDisplayAllCars is then set, which will force the component to exit and the DisplayAllCars component to load.

    componentDidMount() 
    {   
        axios.delete(`${SERVER_HOST}/cars/${this.props.match.params.id}`)
        .then(res => 
        {
            if(res.data)
            {
                if (res.data.errorMessage)
                {
                    console.log(res.data.errorMessage)    
                }
                else // success
                { 
                    console.log("Record deleted")
                }
                this.setState({redirectToDisplayAllCars:true})
            }
            else 
            {
                console.log("Record not deleted")
            }
        })
    }

Server-Side

server/server.js

// Server-side global variables
require(`dotenv`).config({path:`./config/.env`})


// Express
const express = require(`express`)
const app = express()

app.use(require(`body-parser`).json())
app.use(require(`cors`)({credentials: true, origin: process.env.LOCAL_HOST}))


// Routers
app.use(require(`../server/routes/cars`))


// Port
app.listen(process.env.SERVER_PORT, () => 
{
    console.log(`Connected to port ` + process.env.SERVER_PORT)
})


// Error 404
app.use((req, res, next) => {next(createError(404))})

// Other errors
app.use(function (err, req, res, next)
{
    console.error(err.message)
    if (!err.statusCode) 
    {
        err.statusCode = 500
    }
    res.status(err.statusCode).send(err.message)
})

Express allows us to have more than one router file. This allows us to write more structured code, as we can put the route handlers that related to a particular server-side resource into a single file. In this example, we only have one server-side resource (i.e. the cars JSON object). Therefore, we place all of our server-side routes into one file, which is called server/routes/cars.js

// Routers
app.use(require(`../server/routes/cars`))

 

server/routes/cars.js

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



const cars = [{_id:0, model:"Avensis", colour:"Red", year:2020, price:30000},
              {_id:1, model:"Yaris", colour:"Green", year:2010, price:2000},
              {_id:2, model:"Corolla", colour:"Red", year:2019, price:20000},
              {_id:3, model:"Avensis", colour:"Silver", year:2018, price:20000},
              {_id:4, model:"Camry", colour:"White", year:2020, price:50000}]

let uniqueId = cars.length



// read all items from cars JSON
router.get(`/cars/`, (req, res) => 
{   
    res.json(cars)
})


// Read one item from cars JSON
router.get(`/cars/:id`, (req, res) => 
{
    const selectedCars = cars.filter(car => car._id === parseInt(req.params.id))
    
    res.json(selectedCars[0])
})


// Add new item to cars JSON
router.post(`/cars/`, (req, res) => 
{
    let newCar = req.body
    newCar._id = uniqueId
    cars.push(newCar)
    
    uniqueId++
    
    res.json(cars)
})


// Update one item in cars JSON
router.put(`/cars/:id`, (req, res) => 
{
    const updatedCar = req.body
    cars.map(car => 
    {
        if(car._id === parseInt(req.params.id))
        {
            car.model = updatedCar.model
            car.colour = updatedCar.colour
            car.year = updatedCar.year
            car.price = updatedCar.price
        }
    })
    
    res.json(cars)   
})


// Delete one item from cars JSON
router.delete(`/cars/:id`, (req, res) => 
{
    let selectedIndex
    cars.map((car, index) => 
    {
        if(car._id === parseInt(req.params.id))
        {
            selectedIndex = index
        }
    })
    cars.splice(selectedIndex, 1)
    
    res.json(cars)       
})

module.exports = router

Create an Express router.

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

The server-side JSON object will be used to hold the applications's car data.

const cars = [{_id:0, model:"Avensis", colour:"Red", year:2020, price:30000},
              {_id:1, model:"Yaris", colour:"Green", year:2010, price:2000},
              {_id:2, model:"Corolla", colour:"Red", year:2019, price:20000},
              {_id:3, model:"Avensis", colour:"Silver", year:2018, price:20000},
              {_id:4, model:"Camry", colour:"White", year:2020, price:50000}]

We shall allow the user to add new cars to the cars JSON. Each time we add a new car, we shall assign uniqueId to be its id property. We shall then increment uniqueId. We initialise uniqueId to be the first available unique number, which happens to be cars.length

let uniqueId = cars.length

router was created at the top of the file. It is used for all the routes.

// read all items from cars JSON
router.get(`/cars/`, (req, res) => 
{   
    res.json(cars)
})

To read one item from the cars JSON object, we filter all of the items in the JSON against the search car's id.

// Read one item from cars JSON
router.get(`/cars/:id`, (req, res) => 
{
    const selectedCars = cars.filter(car => car._id === parseInt(req.params.id))
    
    res.json(selectedCars[0])
})

To add a new car to the cars JSON object, we push it onto the cars JSON object. We use uniqueId to ensure that each item in the cars JSON object has a unique id.

// Add new item to cars JSON
router.post(`/cars/`, (req, res) => 
{
    let newCar = req.body
    newCar._id = uniqueId
    cars.push(newCar)
    
    uniqueId++
    
    res.json(cars)
})

To modify a car, we search through the cars JSON object until we match the search id. We then modify this item.

// Update one item in cars JSON
router.put(`/cars/:id`, (req, res) => 
{
    const updatedCar = req.body
    cars.map(car => 
    {
        if(car._id === parseInt(req.params.id))
        {
            car.model = updatedCar.model
            car.colour = updatedCar.colour
            car.year = updatedCar.year
            car.price = updatedCar.price
        }
    })
    
    res.json(cars)   
})

To delete a car, we search through the cars JSON object until we match the search id. We then use this item's index to remove it from the cars JSON object.

// Delete one item from cars JSON
router.delete(`/cars/:id`, (req, res) => 
{
    let selectedIndex
    cars.map((car, index) => 
    {
        if(car._id === parseInt(req.params.id))
        {
            selectedIndex = index
        }
    })
    console.log(selectedIndex)
    cars.splice(selectedIndex, 1)
    
    res.json(cars)       
})

We need to make the router available to the server/server.js file.

module.exports = router

Add, edit and delete some data from the cars table. What happens to the table if you refresh the browser? Why do you think this happens?

Clear the brower history and refresh the browser. What happens to the table now? Why do you think this happens?

Exit the npm start command line and then run it again. What happens to the table now? Why do you think this happens?

Exit the nodemon command line and then run it again. What happens to the table now? Why do you think this happens?

 
<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>