tl;dr — This is the third article of the Node is Simple article series. In this article series, I will be discussing how to create a simple and secure NodeJS, Express, MongoDB web application.
To follow the past tutorials, Node is Simple Part 1, and Node is Simple Part 2.
Hello, fellow developers, I am once again, asking you to learn some NodeJS with me. In this tutorial, I will be discussing creating basic CRUD operations in NodeJS. So without further ado, let’s start, shall we?
CREATE endpoint
If you can remember in my previous article, I have created a simple endpoint to POST a name and city of a student and created a record (document) of the student. What I am going to do is enhance what I’ve already done. Let’s update the model, and the service to reflect our needs.
We are going to add some new fields to the student document.
const mongoose = require("../database");
const Schema = mongoose.Schema;
const studentSchema = new Schema(
{
_id: {
type: mongoose.SchemaTypes.String,
unique: true,
required: true,
index: true
},
name: { type: mongoose.SchemaTypes.String, required: true },
city: { type: mongoose.SchemaTypes.String, required: true },
telephone: { type: mongoose.SchemaTypes.Number, required: true },
birthday: { type: mongoose.SchemaTypes.Date, required: true }
},
{ strict: true, timestamps: true, _id: false }
);
const collectionName = "student";
const Student = mongoose.model(collectionName, studentSchema, collectionName);
module.exports = {
Student
};
What I’ve done here is added \id, telephone,_ and birthday as the new fields. And I have disabled the Mongoose default \id_ and specified my own.
Now let’s update the service file.
const { Student } = require("../models");
module.exports = class StudentService {
async registerStudent(data) {
const { _id, name, city, telephone, birthday } = data;
const new_student = new Student({
_id,
name,
city,
telephone,
birthday
});
const response = await new_student.save();
const res = response.toJSON();
delete res.__v;
return res;
}
};
Before we are going to test what we have done, I am going to let you in on a super-secret. If you have experience in developing NodeJS applications, I hope you have come across the Nodemon tool. It restarts the server once you changed the files. But today I am going to tell you about this amazing tool called PM2.
What is PM2?
PM2 is a production-grade process management tool. It has various capabilities such as load balancing (which I will discuss in a later tutorial), enhanced logging features, adding environment variables, and many more. Their documentation is super nifty and worth checking out.
So let’s install PM2 and start using it in our web app.
$ npm install pm2@latest -g
Let’s create ecosystem.config.js file in the project root, which will contain all of our environment variables, keys, secrets, and PM2 configurations.
const fs = require("fs");
const SERVER_CERT = fs.readFileSync(__dirname + "/config/server.cert", "utf8");
const SERVER_KEY = fs.readFileSync(__dirname + "/config/server.key", "utf8");
module.exports = {
apps: [
{
name: "node-is-simple",
script: "./index.js",
watch: true,
env: {
NODE_ENV: "development",
SERVER_CERT,
SERVER_KEY,
HTTP_PORT: 8080,
HTTPS_PORT: 8081,
MONGO_URI: "mongodb://localhost/students"
}
}
]
};
Since we have moved all of our keys as the environment variables, now we have to change /config/index.js file to reflect these changes.
module.exports = {
SERVER_CERT: process.env.SERVER_CERT,
SERVER_KEY: process.env.SERVER_KEY,
HTTP_PORT: process.env.HTTP_PORT,
HTTPS_PORT: process.env.HTTPS_PORT,
MONGO_URI: process.env.MONGO_URI
};
Now, remember, /config/index.js is safe to commit to a public repository. But not the ecosystem.config.js file. Also never commit your private keys to a public repository (But I’ve done it for the demonstration purposes). Protect your secrets like your cash 😂.
Since we have done our initial setup let’s run our application. And one thing to keep in mind. If you start the application, as usual,
$ node index.js
it won’t work. Now we have to use PM2 to start our application because it contains all the environment variables needed for the Node web application. Now go to the project root folder and run the following command.
$ pm2 start
If you see this (figure 1) in your console it is working as it should. Now to see the logs run the following command.
$ pm2 logs
If you see this (figure 2) in your console then the node app is working as it should.
Enough with the PM2
Yes, let’s move to test our enhanced CREATE endpoint.
Now create a request like this (figure 3) and hit Send.
If you see this response (figure 4), it is safe to say everything works as it should. Yay!
Now since we have a CREATE endpoint, let’s add a READ endpoint.
READ endpoint
Now let’s read what’s inside our student collection. We can view all the students who are registered, or we can see details from only one student.
GET /students
Now let’s get all the students’ details. We only get the \id, name,_ and city of the student here. Add these lines to the /controllers/index.js file.
/** @route GET /students
* @desc Get all students
* @access Public
*/
router.get(
"/students",
asyncWrapper(async (req, res) => {
const response = await studentService.getAllStudents();
res.send(response);
})
);
And add these lines (inside the StudentService class) to the /servcies/index.js file.
_async_ getAllStudents() {
_return_ Student.find({}, "\_id name city");
}
Let me give a summary of the above lines. Student.find() is the method to apply queries to the MongoDB. Since we need all the students we pass the empty object as the first argument. As the second argument (which is called a projection) we provide the fields we want to return and the fields we do not want to return. Here I want \id, name,_ and city. We can use “-field\name”_ to provide the field we do not want.
GET /students/:id
Now let’s get all the details from a single student. Now here we are getting all the details of a single student.
Now add these lines to the /controllers/index.js file.
/** @route GET /students/:id
* @desc Get a single student
* @access Public
*/
router.get(
"/students/:id",
asyncWrapper(async (req, res) => {
const response = await studentService.getStudent(req.params.id);
res.send(response);
})
);
In the Express router, we can specify a path parameter via /:param syntax. Now we can access this path parameter via req.params.param. This is basic Express and to get more knowledge on this please refer to the documentation, and it is a great source of good knowledge.
Now add these lines to the /servcies/index.js file.
_async_ getStudent(\_id) {
_return_ Student.findById(\_id, "-\_\_v -createdAt -updatedAt");
}
Here we provide the \id_ of the student to get the details. As the projection, we do not want \_v, createdAt,_ and updatedAt fields.
Now let’s check these endpoints.
I have added two more students’ details, so I got three records.
Now let’s check the single student endpoint.
If you get similar results in figure 6 and figure 8 let’s say it was a success.
Update endpoint
Since we created student records, viewed these student records, now it is time to update these student records.
To PUT, or to PATCH?
So this is the biggest question, to update a resource, should we use PUT or PATCH? The answer is somewhat simple. If you want to update the whole resource every time, use PUT. If you want to update the resource partially, use PATCH. It is that simple. This article clarified this dilemma.
Differences between PUT and PATCH
Since I am going to partially update the student record, I will be using PATCH.
PATCH /students/:id
Now let’s update the /controllers/index.js file.
/** @route PATCH /students/:id
* @desc Update a single student
* @access Public
*/
router.patch(
"/students/:id",
asyncWrapper(async (req, res) => {
const response = await studentService.updateStudent(
req.params.id,
req.body
);
res.send(response);
})
);
After adding this let’s update the /services/index.js file.
_async_ updateStudent(\_id, { name, city, telephone, birthday }) {
_return_ Student.findOneAndUpdate(
{ \_id },
{
name,
city,
telephone,
birthday
},
{
_new_: _true_,
omitUndefined: _true_,
fields: "-\_\_v -createdAt -updatedAt"
}
);
}
Let me give a brief description of what’s going on here. We update the student by the given id, and we provide the name, city, telephone, and birthday data to be updated. As the third argument we provide new: true to return the updated document to us, omitUndefined: true to partially update the resource and fields: “-\_v -createdAt -updatedAt”_ to remove these fields from the returning document.
Now let’s check this out.
So if you get similar results as figure 10 then let’s say yay! Now let’s move on to the final part of this tutorial, which is DELETE.
DELETE endpoint
Since we create, read, and update, now it is time to delete some students 😁.
DELETE /students/:id
Since we should not delete all the students at once (It is a best practice IMO), let’s delete student by the provided student_id.
Now let’s update the /controllers/index.js file.
/** @route DELETE /students/:id
* @desc Delete a single student
* @access Public
*/
router.delete(
"/students/:id",
asyncWrapper(async (req, res) => {
const response = await studentService.deleteStudent(req.params.id);
res.send(response);
})
);
Now let’s update the /services/index.js file.
_async_ deleteStudent(\_id) {
_await_ Student.deleteOne({ \_id });
_return_ { message: \`Student \[${\_id}\] deleted successfully\` };
}
Now the time to see this in action.
If the results are similar to figure 12, then it is safe to assume, the application works as it should.
So here are the current /controllers/index.js file and /services/index.js file for your reference.
const router = require("express").Router();
const asyncWrapper = require("../utilities/async-wrapper");
const StudentService = require("../services");
const studentService = new StudentService();
/** @route GET /
* @desc Root endpoint
* @access Public
*/
router.get(
"/",
asyncWrapper(async (req, res) => {
res.send({
message: "Hello World!",
status: 200
});
})
);
/** @route POST /register
* @desc Register a student
* @access Public
*/
router.post(
"/register",
asyncWrapper(async (req, res) => {
const response = await studentService.registerStudent(req.body);
res.send(response);
})
);
/** @route GET /students
* @desc Get all students
* @access Public
*/
router.get(
"/students",
asyncWrapper(async (req, res) => {
const response = await studentService.getAllStudents();
res.send(response);
})
);
/** @route GET /students/:id
* @desc Get a single student
* @access Public
*/
router.get(
"/students/:id",
asyncWrapper(async (req, res) => {
const response = await studentService.getStudent(req.params.id);
res.send(response);
})
);
/** @route PATCH /students/:id
* @desc Update a single student
* @access Public
*/
router.patch(
"/students/:id",
asyncWrapper(async (req, res) => {
const response = await studentService.updateStudent(
req.params.id,
req.body
);
res.send(response);
})
);
/** @route DELETE /students/:id
* @desc Delete a single student
* @access Public
*/
router.delete(
"/students/:id",
asyncWrapper(async (req, res) => {
const response = await studentService.deleteStudent(req.params.id);
res.send(response);
})
);
module.exports = router;
const { Student } = require("../models");
module.exports = class StudentService {
async registerStudent(data) {
const { _id, name, city, telephone, birthday } = data;
const new_student = new Student({
_id,
name,
city,
telephone,
birthday
});
const response = await new_student.save();
const res = response.toJSON();
delete res.__v;
return res;
}
async getAllStudents() {
return Student.find({}, "_id name city");
}
async getStudent(_id) {
return Student.findById(_id, "-__v -createdAt -updatedAt");
}
async updateStudent(_id, { name, city, telephone, birthday }) {
return Student.findOneAndUpdate(
{ _id },
{
name,
city,
telephone,
birthday
},
{
new: true,
omitUndefined: true,
fields: "-__v -createdAt -updatedAt"
}
);
}
async deleteStudent(_id) {
await Student.deleteOne({ _id });
return { message: `Student [${_id}] deleted successfully` };
}
};
So this is it for this tutorial, and we will meet again in a future tutorial about uploading files to MongoDB using GridFS. As usual, you can find the code here (Check for the commit message “Tutorial 3 checkpoint”).
So until we meet again, happy coding…