This lab consolidates programming knowledge learnt in previous lecture to build a simple Single Page Application, which is a web app that consists of two main parts:
The client-side Vue application is identical to the one developed in section 5-4 ‘Making HTTP requests in clients’.
The server-side is built with Express.js.
The main program creates an Express app, binds the REST API implementation to the path /api
, and serves the client-side Vue application as static files in /dist
.
import express from 'express';
let app = express();
// import the router that implements the REST API
import api from './api.mjs';
app.use('/api', api);
// the Vue application built by Vite, 'npm run build'
// it consumes the REST API
app.use(express.static(__dirname + '/dist'));
const port = 8000;
app.listen(port);
An Express Router is an object to group routes and middlewares. In this example, all requests with the prefix /api
will pass through this router. Thus, app.get('/products', ..)
handles requests for path /api/products
. At the end of the module, we export the router object as default. Notice how the main project imports the router and adds it to the stack of middlewares of the Express app.
import express from 'express';
let api = express.Router();
api.get('/products', async (req, res) => {
const q = "SELECT * FROM Product";
try {
const result = await db.all(q);
res.json(result);
} catch (err) {
res.status(500).json(err);
}
});
// other endpoints ...
export default api;
The route for GET /products
uses a SELECT
query to retrieve all products from the database, and if there are no run-time errors, return the result as JSON data to the client. In case there are errors (e.g. the promise db.all(q)
is rejected), we set the HTTP status code to 500
, and return the error object for debug purpose. (In production code, you should never return error object. This may expose confidential information about your application. Usually, you will create a user-friendly error page.)
The following is the route to handle POST /products
. This route handler creates a product in the database. The data about the new product is passed to the API as JSON data. Therefore, the router has to use the express.json()
middleware to parse the HTTP request payload and save the data in req.body
. Next, basic sanity check is done on the user input, e.g. are all required fields of the new product provided? You may want to do further checking, e.g. are the values in a reasonable range. If there are error about input data, the route handler returns HTTP status code 400
.
api.use(express.json());
api.post('/products', async (req,res) => {
if (req.body.productName==undefined
|| req.body.category==undefined
|| req.body.unitPrice==undefined
|| req.body.unitsInStock==undefined) {
return res.sendStatus(400);
}
let valuesNewProduct = {
$productName: req.body.productName,
$category: req.body.category,
$unitPrice: req.body.unitPrice,
$unitsInStock: req.body.unitsInStock
};
// todo: further checking on valid values
const q1 = 'INSERT INTO Product (productName, category, unitPrice, unitsInStock) VALUES ($productName, $category, $unitPrice, $unitsInStock)';
const q2 = "SELECT * FROM Product WHERE id=$id";
try {
const insResult = await db.run(q1, valuesNewProduct);
const pid = insResult.lastID;
let result = await db.get(q2, {$id: pid});
res.status(200).json(result);
} catch (err) {
res.status(500).json(err);
}
});
The database operations may also generate errors. These will be thrown as exceptions by awake
. We catch these promise rejection errors and return the HTTP status code 500
.
If there is no error, we return a JSON string of the newly created product to the client-side.
For the implementation of other routes in the REST API (e.g. PUT
and DELETE
), please refer to the source code.