Skip to main content

Lab-2-5

(2% of the course mark)

MySQL MVC Express Lab

  • In this lab, students will refactor the Express MVC app that was built in Lab 1-3. The lab will focus on integrating the mysql2 package into the Express MVC app.

Lab objectives

  • Learn the technique of properly in integrating the mysql2 package into the Express MVC app.

  • Use the factory / singleton pattern to build and share database connection and querying capabilites.

  • Use connection pooling effectively.

Build Schema

  1. Open MySQL Workbench and run the following SQL commands:
-- Drop the database if it exists
DROP DATABASE IF EXISTS BACKEND_DB;
-- Create the database
CREATE DATABASE BACKEND_DB;
-- Use the database
USE BACKEND_DB;
-- Create the USERS table
CREATE TABLE USERS (
ID INT AUTO_INCREMENT PRIMARY KEY,
USER_NAME VARCHAR(50) UNIQUE NOT NULL,
EMAIL VARCHAR(50) UNIQUE NOT NULL,
CREATED_AT TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Insert a row into the USERS table
INSERT INTO
USERS (USER_NAME, EMAIL)
VALUES
('bill.gates', 'bill.gates@microsoft.com'),
('elon.musk', 'elon.musk@tesla.com');
-- Verify
SELECT * FROM USERS;
Database Objects

Ensure that all the database objects are created successfully before proceeding to the next step.

Create Node.js Express app

  1. Open VSCode and create a folder named MySql-Express-App.

  2. Open the terminal and change the directory to MySql-Express-App.

  3. Initialize the app by running the following command:

npm init -y
  1. Install typescript by running the following commands:
npm install typescript -D
npm install @types/node -D
  1. Install mysql2 by running the following command:
npm install mysql2
  1. Initialize tsconfig.json by running the following command:
npx tsc --init
  1. Modify tsconfig.json and uncomment outDir.

  2. Modify tsconfig.json and add the following property:

"removeComments": true
  1. Install nodemon by running the following command:
npm install nodemon -D
  1. Install dotenv by running the following command:
npm install dotenv
  1. Create an .env file in the root of your project and enter the following.
PORT=3000
SQL_HOST=localhost
SQL_USER=root
SQL_PASSWORD=password
SQL_DB_NAME=BACKEND_DB
SQL_CONNECTION_LIMIT=10
  1. Install express by running the following commands:
npm install express
npm install @types/express -D
  1. Configure package.json by adding a new entry to the scripts property.
"build": "tsc",
"start": "nodemon ./dist/index",
  1. Configure package.json by adding a new property.
"type": "module"
  1. Create a file named index.ts and add the following code:
// Developer:
// Purpose:

import * as dotenv from "dotenv";
dotenv.config();

import express, { type Request, type Response } from "express";

const PORT: number = process.env.PORT ? parseInt(process.env.PORT) : 3000;
const app = express();

app.all("/{*catchAll}", (req: Request, res: Response) => {
res.status(404).json({
message: `Invalid route (${req.url}) or HTTP method (${req.method}).`,
});
});

app.listen(PORT, () => {
console.log(`Server is running at http://localhost:${PORT}`);
process.on("SIGTERM", shutdown);
process.on("SIGINT", shutdown);
});

function shutdown() {
console.log(`Server is shutting down.`);
process.exit(0);
}
  1. Build the app by running the following command:
npm run build
  1. Start the app by running the following command:
npm run start
Express app status

Verify that your Express app starts without errors before proceeding to the next step.

Express app MVC

  1. In the root folder of the app, create a folder named lib.

  2. In the lib folder, create a file named DBConnectionFields.ts and add the following code:

// Developer:
// Purpose:

export class DBConnectionFields {
host: string = "";
user: string = "";
password: string = "";
database: string = "";
connectionLimit: number = 0;

constructor(
host: string,
user: string,
password: string,
database: string,
connectionLimit: number
) {
this.host = host;
this.user = user;
this.password = password;
this.database = database;
this.connectionLimit = connectionLimit;
}
}
  1. In the lib folder, create a file named MySqlDB.ts and add the following code:
// Developer:
// Purpose:

import mysql, { type Pool } from "mysql2/promise";
import { DBConnectionFields } from "./DBConnectionFields.js";

export class MySqlDB {
dbConnectionFields: DBConnectionFields | null = null;
pool: Pool | null = null;

constructor() {}

setDbConnectionFields(dbConnectionFields: DBConnectionFields) {
this.dbConnectionFields = dbConnectionFields;
}

async connect() {
let connectResponse = {
isConnected: false,
message: "",
};

try {
if (this.dbConnectionFields != null) {
this.pool = mysql.createPool({
host: this.dbConnectionFields.host,
user: this.dbConnectionFields.user,
password: this.dbConnectionFields.password,
database: this.dbConnectionFields.database,
waitForConnections: true,
connectionLimit: this.dbConnectionFields.connectionLimit,
});

connectResponse.isConnected = true;
connectResponse.message = `Connected to: ${this.dbConnectionFields.database}.`;
}
} catch (error: any) {
connectResponse.message = `Could not connect to: ${this.dbConnectionFields?.database}.`;
} finally {
return connectResponse;
}
}

async query(sqlStatement: string, options: [] = []) {
let queryResult: any;

try {
if (this.pool != null) {
queryResult = await this.pool.execute(sqlStatement, options);
}
} catch (error: any) {
console.log("code:", error.code);
console.log("errno:", error.errno);
console.log("sqlState:", error.sqlState);
console.log("sqlMessage:", error.sqlMessage);
console.log("sql:", error.sql);
} finally {
return queryResult;
}
}

async disConnect() {
let disConnectResponse = {
isDisconnected: false,
message: "",
};

try {
if (this.pool != null) {
await this.pool.end();
}

disConnectResponse.isDisconnected = true;
disConnectResponse.message = `Disconnected to: ${this.dbConnectionFields?.database}.`;
} catch (error) {
disConnectResponse.message = `Could not disconnect to: ${this.dbConnectionFields?.database}.`;
} finally {
return disConnectResponse;
}
}
}

// This cached by node and returns the same instance all the time, acts like a singleton pattern
export const mySqlDB = new MySqlDB();
  1. In the root folder of the app, create a folder named models.

  2. In the models folder, create a file named userModel.ts and add the following code:

// Developer:
// Purpose:

import { mySqlDB } from "../lib/MySqlDB.js";

export interface User {
id: number;
userName: string;
email: string;
createdAt: Date;
}

export const UserModel = {
findAll: async (): Promise<User[]> => {
const queryResult = await mySqlDB.query("SELECT * FROM USERS");

if (queryResult) {
if (queryResult[0]) {
const mapResult = queryResult[0].map((user: any) => ({
id: user.ID,
userName: user.USER_NAME,
email: user.EMAIL,
createdAt: new Date(user.CREATED_AT),
}));

return mapResult;
}
}

return [];
},

findById: async (id: number): Promise<User | undefined> => {
const options = [id];
const queryResult = await mySqlDB.query(
"SELECT * FROM USERS WHERE ID = ?",
options as any
);

if (queryResult) {
if (queryResult[0]) {
const mapResult = queryResult[0].map((user: any) => ({
id: user.ID,
userName: user.USER_NAME,
email: user.EMAIL,
createdAt: new Date(user.CREATED_AT),
}));

return mapResult[0];
}
}

return undefined;
},

findByUserName: async (userName: string): Promise<User | undefined> => {
const options = [userName];
const queryResult = await mySqlDB.query(
"SELECT * FROM USERS WHERE USER_NAME = ?",
options as any
);

if (queryResult) {
if (queryResult[0]) {
const mapResult = queryResult[0].map((user: any) => ({
id: user.ID,
userName: user.USER_NAME,
email: user.EMAIL,
createdAt: new Date(user.CREATED_AT),
}));

return mapResult[0];
}
}

return undefined;
},

create: async (userName: string, email: string): Promise<boolean> => {
const options = [userName, email];
const queryResult = await mySqlDB.query(
"INSERT INTO USERS (USER_NAME, EMAIL) VALUES(?, ?)",
options as any
);

if (queryResult) {
if (queryResult[0]?.affectedRows) {
return true;
}
}

return false;
},

update: async (
id: number,
userName: string,
email: string
): Promise<boolean> => {
const options = [userName, email, id];
const queryResult = await mySqlDB.query(
"UPDATE USERS SET USER_NAME = ?, EMAIL = ? WHERE ID = ?",
options as any
);

if (queryResult) {
if (queryResult[0]?.affectedRows) {
return true;
}
}

return false;
},

delete: async (id: number): Promise<boolean> => {
const options = [id];

const queryResult = await mySqlDB.query(
"DELETE FROM USERS WHERE ID = ?",
options as any
);

if (queryResult) {
if (queryResult[0]?.affectedRows) {
return true;
}
}

return false;
},
};
  1. In the root folder of the app, create a folder named services.

  2. In the services folder, create a file named userService.ts and add the following code:

// Developer:
// Purpose:

import { UserModel } from "../models/userModel.js";

export const UserService = {
getAll: async () => await UserModel.findAll(),
getById: async (id: number) => await UserModel.findById(id),
getByUserName: async (userName: string) =>
await UserModel.findByUserName(userName),
create: async (userName: string, email: string) =>
await UserModel.create(userName, email),
update: async (id: number, userName: string, email: string) =>
await UserModel.update(id, userName, email),
delete: async (id: number) => await UserModel.delete(id),
};
  1. In the root folder of the app, create a folder named controllers.

  2. In the controllers folder, create a file named userController.ts and add the following code:

// Developer:
// Purpose:

import { type Request, type Response } from "express";
import { UserService } from "../services/userService.js";

export const UserController = {
getAll: async (req: Request, res: Response): Promise<void> => {
res.json(await UserService.getAll());
},

getById: async (req: Request, res: Response): Promise<void> => {
const user = await UserService.getById(Number(req.params.id));
user ? res.json(user) : res.status(404).json({ message: "User not found" });
},

getByUserName: async (req: Request, res: Response): Promise<void> => {
const user = await UserService.getByUserName(String(req.params.userName));
user ? res.json(user) : res.status(404).json({ message: "User not found" });
},

create: async (req: Request, res: Response): Promise<void> => {
const { userName, email } = req.body;
if (!userName) {
res.status(400).json({ message: "userName is required" });
return;
}

if (!email) {
res.status(400).json({ message: "email is required" });
return;
}

(await UserService.create(userName, email))
? res.status(201).send()
: res.status(500).json({
message: "Internal Server Error, contact the system administrator",
});
},

update: async (req: Request, res: Response): Promise<void> => {
const { userName, email } = req.body;
(await UserService.update(Number(req.params.id), userName, email))
? res.status(204).send()
: res.status(404).json({ message: "User not found" });
},

delete: async (req: Request, res: Response): Promise<void> => {
(await UserService.delete(Number(req.params.id)))
? res.status(204).send()
: res.status(404).json({ message: "User not found" });
},
};
  1. In the root folder of the app, create a folder named routes.

  2. In the routes folder, create a file named userRouter.ts and add the following code:

// Developer:
// Purpose:

import { Router } from "express";
import { UserController } from "../controllers/userController.js";

const router = Router();

router.get("/", UserController.getAll);
router.get("/:id", UserController.getById);
router.get("/username/:userName", UserController.getByUserName);
router.post("/", UserController.create);
router.put("/:id", UserController.update);
router.delete("/:id", UserController.delete);

export default router;
  1. Update index.ts and add the following code:
// Developer:
// Purpose:

import * as dotenv from "dotenv";
dotenv.config();

import express, { type Request, type Response } from "express";
const PORT: number = process.env.PORT ? parseInt(process.env.PORT) : 3000;

import { mySqlDB } from "./lib/MySqlDB.js";
import { DBConnectionFields } from "./lib/DBConnectionFields.js";
import userRouter from "./routes/userRouter.js";

const app = express();
const dBConnectionFields = new DBConnectionFields(
String(process.env.SQL_HOST),
String(process.env.SQL_USER),
String(process.env.SQL_PASSWORD),
String(process.env.SQL_DB_NAME),
Number(process.env.SQL_CONNECTION_LIMIT)
);

mySqlDB.setDbConnectionFields(dBConnectionFields);
const connectResponse = await mySqlDB.connect();
console.log(connectResponse);

app.use(express.json());
app.use("/api/users", userRouter);

app.all("/{*catchAll}", (req: Request, res: Response) => {
res.status(404).json({
message: `Invalid route (${req.url}) or HTTP method (${req.method}).`,
});
});

app.listen(PORT, () => {
console.log(`Server is running at http://localhost:${PORT}`);
process.on("SIGTERM", shutdown);
process.on("SIGINT", shutdown);
});

async function shutdown() {
console.log(`Server is shutting down.`);
const disConnectResponse = await mySqlDB.disConnect();
console.log(disConnectResponse);
process.exit(0);
}
  1. Build the app by running the following command:
npm run build

Postman testing

  1. Use Postman to perform the following testing scenarios:
MethodEndpointScreenshot
GET/api/usersget-all.png
GET/api/users/valid-user-idget-one01.png
GET/api/users/invalid-user-idget-one02.png
GET/api/users/userName/valid-user-nameget-one03.png
GET/api/users/userName/invalid-user-nameget-one04.png
POST/api/userspost.png
PUT/api/users/valid-user-idput01.png
PUT/api/users/invalid-user-idput02.png
DELETE/api/users/valid-user-iddelete.png

Sample request body for POST and PUT

{
"userName": "firstname.lastname",
"email": "firstname.lastname@email.com"
}

Implement Lab-2-3 Library app (MySql-Express-App)

  1. Modify the .env file of the MySql-Express-App and update the value of SQL_DB_NAME to the name of the library app schema.
SQL_DB_NAME=NAME_OF_LIBRARY_APP_SCHEMA
  1. Refactor MySql-Express-App and create new routes, controllers, services, and models:
  • Allow patrons to borrow and return books.

  • View current book inventory levels.

  1. Build and run the modified app and use Postman to test the scenarios above and name the screenshots as borrow.png, return.png and inventory.png.

Submission

  1. Create a folder named submit.
Delete node_modules

Delete the node_modules folder on any app that you are submitting.

  1. Copy the MySql-Express-App folder and all the screenshots (get-all.png, get-one01.png, get-one02.png, get-one03.png, get-one04.png, post.png, put01.png, put02.png, delete.png, borrow.png, return.png and inventory.png) to the submit folder.

  2. Create a zip file of the submit folder.

  3. Navigate back to where the lab was originally downloaded, there should be a Submissions section (see below) where the zip file can be uploaded.

submission