Copyright Derek O'Reilly, Dundalk Institute of Technology (DkIT), Dundalk, Co. Louth, Ireland.
On the server-side, a RESTful (Representational State Transfer) API associates a HTTP verb (get, post, put, delete, patch) to an endpoint on the client-side and to a function that is called to handle the server-side endpoint.
An endpoint is a string that identifies a physical or logical resource. An endpoint looks similar to a URL. Endpoints can also be called URIs (Uniform Resource Identifier).
HTTP verb | CRUD | Action | Endpoint (URI) | Examples |
---|---|---|---|---|
get | read | returns requested data | /cars/ |
returns the details of all cars |
/cars/123 |
returns the details of product 123 | |||
post | create | creates a new record | /cars/corrolla/red/2020/25000 | creates a product with model=corolla, colour=red, year=2020 and price=25000 |
put | update | updates an entire existing record | /cars/123 |
updates product 123 with the contents of the body (also called the payload) that is passed to the server with the api call |
/cars/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 | /cars/123 | updates product 123 with the contents of the body (also called the payload) that is passed to the server with the api call |
/cars/price/123/20000 | updates the price of product 123 with the value 20000 | |||
delete | delete | deletes an existing record | /cars/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.
In our projects, where do you think the client-side endpoints are handled?
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 the server-side RESTful APIs are written using the following template:
router.HTTP_verb(endpoint, middleware_function(req, res))
const router = require(`express`).Router()
router.get(`/cars/:id`, (req, res) => { console.log(req.params.id) })
router.get(`/cars/:id`, (req, res) => { // code to get the product and assign it to a JSON object called selectedProduct ... res.json(selectedProduct) // return the json object called selectedProduct })
We create one router file for each resource that we wish to access. We place router files into the server/routes folder. In our Cars Worked Example, we currently have one resource (ie the cars JSON array object). We shall call its router file server/routes/cars.js
In order to use routers in a node Express application, we must:
const router = require(`express`).Router() ... module.exports = router
...
app.use(require(`./routes/cars`))
...
We use axios on the client-side to communicate with server-side Express routes.
We shall store the server-side URL in a variable called SERVER_HOST. We shall make a file called "client/src/config/global_constants.js" to hold this (and any other client-side global variables that we use in our project), as shown below:
...
export const SERVER_HOST = `http://localhost:4000`
...
In order to use SERVER_HOST in a client-side React Component file, we must import it.
import {SERVER_HOST} from "../config/global_constants"
...
Other than the inclusion of SERVER_HOST at the beginning of the client-side endpoint, the client-side and server-side endpoints must match, as shown below:
// client-side axios.get(`${SERVER_HOST}/cars`) .then(res => { ... }) // server-side router.get(`/cars`, (req, res) => { ... })
Each RESTful API HTTP_verb has a client-side axios endpoint and a server-side endpoint associated with it.
// client-side endpoint axios.get(`${SERVER_HOST}/cars`) .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 endpoint router.get(`/cars`, (req, res) => { console.log(req) })
If we are getting one record, we can include the record's id in the client-side and server-side endpoints.
// client-side endpoint axios.get(`${SERVER_HOST}/cars/${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 endpoint router.get(`/cars/:id`, (req, res) => { console.log(req.params.id) // we can use req.params.id on the server side })
// client-side endpoint axios.post(`${SERVER_HOST}/cars/${model}/${colour}/${year}/${price}`) .then(res => { if(!res.data) { console.log("Record not added") } }) // server-side endpoint router.post(`/cars/: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 endpoint axios.post(`${SERVER_HOST}/cars`, newProductJSON) .then(res => { if(!res.data) { console.log("Record not added") } }) // server-side endpoint // On the server-side, the newProductJSON will be held in req.body router.post(`/cars`, (req, res) => { console.log(req.body) // req.body holds the properties from newProductJSON })
// client-side endpoint axios.put(`${SERVER_HOST}/cars/${id}`, updatedProductJSON) .then(res => { if(!res.data) { console.log("Record not modified") } }) // server-side endpoint router.put(`/cars/:id`, (req, res) => { console.log(req.params.id) console.log(req.body.model) // req.body holds the properties from updatedProductJSON })
// client-side endpoint axios.patch(`${SERVER_HOST}/cars/${id}`, {price: newPrice}) .then(res => { if(!res.data) { console.log("Record not modified") } }) // server-side endpoint router.patch(`/cars/:id`, (req, res) => { console.log(req.params.id) console.log(req.body.price) // req.body holds the value of the JSON object {price: newPrice} })
// client-side endpoint axios.delete(`${SERVER_HOST}/cars/${id}`) .then(res => { if(!res.data) { console.log("Record not deleted") } }) // server-side endpoint router.delete(`/cars/:id`, (req, res) => { console.log(req.params.id) })
Open the axios project from the previous section in these notes. Follow the instructions in the notes above to create a file server/routes/cars.js and then copy the code below into the server/routes/cars.js file.
let 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 // use this to ensure that we give a unique id to each new object that is added to cars.The above code will allow us to store the JSON data in a variable (called cars) on the server-side.
Change the "axios" example code to implement server-side routing, as shown below:
Add to the code above to implement server-side routing that allows the user to add, modify and delete items from the cars JSON data. Your client-side web application user interface should look similar to the one shown below:
The full project code for the "Cars" Worked Example that is described below 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 endpoints. The server-side endpoints will access the cars JSON object and return the relavent data to the client-side.
// 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.
//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 endpoints import React, {Component} from "react" export default class Button extends Component { render() { return ( <span tabIndex="0" className={this.props.className} onClick={(event) => {this.props.onClick(event)}}> {this.props.value} </span> ) } }
Button 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.
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>
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>
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() 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> <Button 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 endpoint can give to any of the Axios methods:
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 endpoint (i.e. if the res.data from the server-side endpoint 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") } })
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() 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> <Button 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.
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 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(`./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 endpoint 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 endpoints into one file, which is called server/routes/cars.js
// Routers
app.use(require(`./routes/cars`))
const router = require(`express`).Router() let 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.
let 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 endpoints.
// 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
Copyright Derek O' Reilly, Dundalk Institute of Technology (DkIT), Dundalk, Co. Louth, Ireland.