Writing Code for REST API Service with Node JS
Setup Node Project
Open a git bash terminal and issue the following commands
# Create a new folder for the project
$ mkdir rest_node_mysql
$ cd rest_node_mysql
# Initialize this folder for Git
$ git init
# Create some new files (to be edited later in vscode)
$ touch .gitignore
$ touch .env
# Create a new Node project
$ npm init -y
# Install node packages
$ npm i express mysql
$ npm i nodemon dotenv --save-dev
# Open the project into vscode
$ code .
Edit the file package.json
in VSCode
{
"name": "backend_node_msyql",
"version": "1.0.0",
"description": "",
"main": "api/server.js",
"scripts": {
"development": "nodemon api/server.js",
"start": "node api/server.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.17.1",
"mysql": "^2.18.1"
},
"devDependencies": {
"dotenv": "^8.2.0",
"nodemon": "^2.0.4"
}
}
Edit the file .ignore
in VSCode
node_modules/
.env
Create some folders and files from VSCode or by bash command line as follows
$ mkdir dev
$ mkdir dev/rest_client
$ touch dev/rest_client/request.rest
$ mkdir api
$ mkdir api/controllers
$ mkdir api/models
$ touch api/server.js
$ touch api/database.js
$ touch api/controllers/todos_controller.js
$ touch api/models/todos_model.js
Define the main program
Complete version of file ./api/server.js
const port = process.env.PORT || 3000;
const express = require("express");
const todosRouter = require("./controllers/todos_controller");
const app = express();
app.use(express.json());
app.get("/", (req, res, next) => res.send(`Server is running on port ${port}`));
app.use("/todos", todosRouter);
app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});
Setup Database Connection
Example content of the file .env
. These will be the mock environment variables, used only during development. In production mode, they will be provided directly by the platform where this application runs on
# The port numberthe server listens to
PORT = 3000
# Configuration for Localhost database
DATABASE_HOST = localhost
DATABASE_USER = root
DATABASE_PASSWORD =
DATABASE_NAME = tododb
DATABASE_CONNECTION_LIMIT = 10
# Configuration for live database on heroku
# DATABASE_HOST=us-cdbr-east-05.cleardb.net
# DATABASE_USER=cq27fd50e67302
# DATABASE_PASSWORD=d4d6f8g9
# DATABASE_NAME=heroku_7d5414e20b2b749
# DATABASE_CONNECTION_LIMIT=10
Complete version of file ./api/database.js
require("dotenv").config(); // Uncomment this when deploying to heroku
const mysql = require("mysql");
// Create database connection between nodejs and mysql
// Here, we are implementing the Database class with Singleton design pattern
// Singleton is a design pattern where we create only a single instance (or object) from a class
class Database {
constructor() {
if (this.instance) return this.instance; // This is the key idea of implementing singleton. Return the same instance (i.e. the one that has already been created before)
// We only proceedd to the following lines only if no instance has been created from this class
Database.instance = this;
// We use Environment variables to configure the db connection, so that later when we deploy to heroku
// we can configure the database based on heroku configuration
this.pool = mysql.createPool({
connectionLimit: process.env.DATABASE_CONNECTION_LIMIT || 10,
host: process.env.DATABASE_HOST,
user: process.env.DATABASE_USER,
password: process.env.DATABASE_PASSWORD,
database: process.env.DATABASE_NAME,
});
}
// This method is only a convinience wrapper. It is intended to convert Callback-based style to Promise-based style
// to acheive non-blocking code (or asynchronous code)
//
// Side node: Asynchronous or Non-blocking programming can be done with Callback style or Promise based style
// mysql library is written based on Callback style. However, asynchronous programming is much easier
// done with Promise based as later you can use the async / await sytax
query(sql, params) {
return new Promise((resolve, reject) => {
this.pool.query(sql, params, (err, result) => {
if (err) return reject(err);
return resolve(result);
});
});
}
}
module.exports = new Database();
Define Model Classes
Complete version of file ./api/models/todos_model.js
const database = require("../database");
// Here, we are implementing the class with Singleton design pattern
// Singleton is a design pattern where we create only a single instance (or object) from a class
class TodoModel {
constructor() {
if (this.instance) return this.instance; // This is the key idea of implementing singleton. Return the same instance (i.e. the one that has already been created before)
// We only proceedd to the following lines only if no instance has been created from this class
TodoModel.instance = this;
}
get() {
return database.query("SELECT * FROM todos");
}
async getById(id) {
const rows = await database.query("SELECT * FROM todos WHERE id = ?", [id]);
return rows[0];
}
create(todo) {
return database.query(
"INSERT INTO todos (title, completed) VALUES (?, ?)",
[todo.title, todo.completed]
);
}
delete(id) {
return database.query("DELETE FROM todos WHERE id = ? ", [id]);
}
update(id, todo) {
const fields = [];
const params = [];
for (const attribute in todo) {
fields.push("?? = ?");
params.push(attribute, todo[attribute]);
}
const stmt = `UPDATE todos SET ${fields.join(", ")} WHERE id = ?`;
return database.query(stmt, [...params, parseInt(id)]);
}
}
module.exports = new TodoModel();
Define Routers
Complete version of file ./api/controllers/todos_controller.js
const todosModel = require("../models/todos_model");
const express = require("express");
const router = express.Router();
// Get all todos
router.get("/", async (req, res, next) => {
try {
const result = await todosModel.get();
res.json(result);
} catch (e) {
console.log(e);
}
});
// Get one todo
router.get("/:id", async (req, res, next) => {
try {
const result = await todosModel.getById(req.params.id);
if (!result) return res.sendStatus(404);
res.json(result);
} catch (e) {
console.log(e);
}
});
// Create a new todo
router.post("/", async (req, res, next) => {
try {
const createResult = await todosModel.create(req.body);
if (!createResult.affectedRows) return res.sendStatus(409);
const result = await todosModel.getById(createResult.insertId);
res.status(201).json(result);
} catch (e) {
console.log(e);
}
});
// Delete a todo
router.delete("/:id", async (req, res, next) => {
try {
const result = await todosModel.delete(req.params.id);
if (!result.affectedRows) return res.sendStatus(404);
res.sendStatus(200);
} catch (e) {
console.log(e);
}
});
// Update a todo
router.patch("/:id", async (req, res, next) => {
try {
const updateResult = await todosModel.update(req.params.id, req.body);
if (!updateResult.affectedRows) return res.sendStatus(404);
const result = await todosModel.getById(req.params.id);
res.json(result);
} catch (e) {
console.log(e);
}
});
module.exports = router;
Testing the REST API server
Use REST Client to test the API server
Edit the file file ./dev/rest_client/request.rest
# @baseUrl = http://localhost:3000
### Getting the list of todos
GET http://localhost:3000/todos
### Getting a todo of given id
GET http://localhost:3000/todos/31
### Create a new todo
POST http://localhost:3000/todos
Content-Type: application/json
{
"title": "New todo 3",
"completed" : false
}
### Update the status of a given todo (whether is completed or not)
PATCH http://localhost:3000/todos/21
Content-Type: application/json
{
"completed": false
}
### Deleting a given todo
DELETE http://localhost:3000/todos/21