Implement a real database using MongoDB
MongoDB is a popular NoSQL database that is commonly used with Node.js. In this chapter, you will install and run your own copy of MongoDB and read from and write to it.
Software Engineering Team Lead and Director of Cloudsure
Objectives
- Learn some MongoDB.
- Install and run your own copy of MongoDB community.
- Populate the database with in-memory data.
- Learn how to read and write from and to MongoDB.
Get started
Installation
Install and run the MongoDB community service (version 6.0). This is the actual database and its service that will run on your machine or on a server somewhere.
You are going to make a directory inside the server to store all documents to while in development mode.
mkdir server/db
Git mustn't know about this addition to your project so you can ignore the directory from Git.
server/db/
Start the service
If you don't have the service running globally then you can start the service and point it to your DB path to the newly created directory.
mongod --dbpath ./server/db/
REPL environment
The MongoDB Shell, mongosh
, is a fully functional JavaScript and Node.js 16.x
REPL environment for interacting with MongoDB deployments.
You can use the MongoDB Shell to test queries and operations
directly with your database.
mongosh
Create your database with the use
keyword.
use stargazers-db
Populate some values using the in-memory data file from earlier.
db.reviews.insertMany([
{
"slug": "joes-snack-shop",
"title": "Joe's Snack Shop",
"abstract": "Lorem ipsum, dolor sit amet consectetur adipisicing elit.",
"rating": 3.5,
"ratings": [
{
"total": 3.5
}
]
},
{
"slug": "gerrys-tv",
"title": "Gerry's TV",
"abstract": "Lorem ipsum, dolor sit amet consectetur adipisicing elit.",
"rating": 3.5,
"ratings": [
{
"total": 4
},
{
"total": 3
}
]
},
{
"slug": "pieters-flower-shack",
"title": "Pieter's Flower Shack",
"abstract": "Lorem ipsum, dolor sit amet consectetur adipisicing elit."
}
])
The following code will get all reviews and format the JSON because of the pretty()
function.
db.reviews.find({}).pretty()
Integrate with Express
Install the mongodb npm package.
npm install mongodb
Create a new file to generically connect to the MongoDB instance and execute queries. The collection returned from the db is passed into the function so that data is available to the calling code.
😱 Note that the hardcoded values will be removed later.
touch src/db.js
import { MongoClient } from 'mongodb';
// 😱 Fret not, it will be configured later on
const url = 'mongodb://127.0.0.1:27017';
const dbName = 'stargazers-db';
const client = new MongoClient(url);
const closeConnection = () => {
client.close();
console.info(`Successfully closed MongoDB instance to ${url}`);
};
const openConnection = async () => {
await client.connect();
console.info(`Successfully connected to MongoDB instance at ${url}`);
};
const withCollection = async (name, executeQuery) => {
try {
const db = client.db(dbName);
const collection = db.collection(name);
return await executeQuery(collection);
} catch (e) {
closeConnection();
throw e;
}
};
export { openConnection, closeConnection, withCollection };
Integrate with Reviews collection
A review is a piece of data (perhaps a company or thing) that can be rated and commented on.
This file will be responsible for all things review related like getting a review by its slug, getting all reviews, calculating the average when rating the review and commenting on a review.
import { withCollection } from './db.js';
const collectionName = 'reviews';
// Calculates the average rating from the ratings list in the collection.
// Each rating in the array will have a total that needs to be used in the calculation.
const calculateAverage = (ratings) => {
if (!ratings || ratings.length === 0) return 0;
// Calculate the total from the ratings in the array by reducing the sum of each
// total as a float value.
const total = ratings.reduce((acc, { total }) => {
return acc + parseFloat(total);
}, 0);
const average = total / ratings.length;
// Only whole and half numbers are used in the rating.
// If the modulus of 0.5 of the average is 0 then the average valid.
if (average % 0.5 === 0) {
return average;
}
// This calculation is somewhat more complicated.
// Get the modulus of 1 from the average to get the fraction.
// Example 2.25 % 1 = 0.25 and 5.75 % 1 = 0.75
// Multiply the modulus result by 10 to get a number greater than 0.
// Floor the number to remove the fraction bits.
// Now we can see if the average must be rounded up or down by checking if the
// result is >= 5 or < 5.
return Math.floor((average % 1) * 10) >= 5
// Remove the result of the 0.5 modulus calculation from the average
// 5.75 % 0.5 = 0.25
// 5.75 - (5.75 % 0.5) = 5.5
? average - (average % 0.5)
: Math.floor(average);
};
// Returns a single review from the collection
const getReviewBySlug = (slug) => {
return withCollection(collectionName, async (collection) => {
return await collection.findOne({ slug });
});
};
// Returns all reviews from the collection
const getAllReviews = () => {
return withCollection(collectionName, async (collection) => {
return await collection.find({}).toArray();
});
};
// Rates a specific review
const rateReview = (slug, rating) => {
return withCollection(collectionName, async (collection) => {
const total = parseFloat(rating, 0);
const review = await getReviewBySlug(slug);
const average = calculateAverage([...(review?.ratings || []), { total }]);
return await collection.updateOne(
{
slug,
},
{
// set the average on the rating field in the document
$set: {
rating: average,
},
// push the total field and value to the ratings array
$push: {
ratings: {
total,
},
},
},
);
});
};
// Comment on a specific review
const commentOnReview = (slug, { name, email, comment }) => {
// Indicate which fields should be marked and checked as mandatory
const requiredFields = [
{ field: 'Name', value: name },
{ field: 'Comment', value: comment },
];
// If every mandatory field has a value then the form is valid
const isValid = requiredFields.every((field) => field.value);
if (!isValid) {
// Concatenate a string of missing fields to be sent as an error message downstream
const fields = requiredFields
.reduce((acc, prev) => {
return `${acc} ${prev.value ? '' : `[${prev.field}]`}`;
}, '')
.trim();
throw new Error(`We are missing values for ${fields}.`);
}
return withCollection(collectionName, async (collection) => {
return await collection.updateOne(
{ slug },
{
// push the comment and timestamp to the comments field in the document
$push: {
comments: {
name,
email,
comment,
timestamp: new Date().getTime(),
},
},
},
);
});
};
// Named exports for publicly exposed functionality
export { getReviewBySlug, getAllReviews, rateReview, commentOnReview };
Create endpoints
Note that you need to specify the file extension when importing
db
because you set the type to"module"
in package.json.
Now that you have a database, you can create functional production-ready endpoints. You will be creating endpoints to:
- get a list of all the reviews in the database
- get a single review
- rate a review
- leave a comment on a review
import express from 'express';
import { openConnection as openMongoDbConnection } from './db.js';
import {
commentOnReview,
getAllReviews,
getReviewBySlug,
rateReview,
} from './reviews.js';
const app = express();
app.use(express.json());
app.get('/api/reviews', async (_, res) => {
const reviews = await getAllReviews();
res.json(reviews);
});
app.get('/api/reviews/:slug', async (req, res) => {
const { slug } = req.params;
const review = await getReviewBySlug(slug);
if (review) {
res.json(review);
} else {
res.status(404).json({
query: req.params,
error: `Review could not be found in the database.`,
});
}
});
app.put('/api/review/:slug/rate/:rating', async (req, res) => {
const { slug, rating } = req.params;
await rateReview(slug, rating);
const review = await getReviewBySlug(slug);
if (review) {
res.json(review);
} else {
res.status(404).json({
query: req.params,
error: `Review could not be rated because it cannot be found in the database.`,
});
}
});
// Comment on a review. Validation takes place inside the commentOnReview
// so when an exception is caught, a bad request is returned to the client
// along with the assumed validation error message.
app.post('/api/review/:slug/comment', async (req, res) => {
const { slug } = req.params;
try {
await commentOnReview(slug, req.body);
} catch (e) {
res.status(400).json({
query: req.params,
error: e.message,
});
return;
}
const review = await getReviewBySlug(slug);
if (review) {
res.json(review);
} else {
res.status(404).json({
query: req.params,
error: `Review could not be commented on because it cannot be found in the database.`,
});
}
});
// Only open the Mongo DB connection once
app.listen('3001', async () => {
console.log('Listening on http://localhost:3001');
await openMongoDbConnection();
});
Delete documents
If you need to, you can always delete the files and create them again.
db.reviews.deleteMany({})
Next steps
In the next chapter, you will get the frontend to communicate with the backend by exposing an API.
References
- MongoDB Crash Course - Traversy Media on YouTube
- MongoDB community installation: Official documentation
- MongoDB Shell (mongosh) - Official documentation
- MongoDB npm package - npm