import { addBanner, addArticle, addTitle, addHeader, addParagraph, addClassParagraph, addSubHeader, addOrderedList, addUnorderedList, addBlockquote, addInset, addInsetList, addInsetCodeListing, addInsetBulletList, addImageWithCaption, addButtonGroup, addSidebar, addSyntax, menu, global_menu } from '/scripts/import.js'; import { local_menu } from '/scripts/webdev.js'; const heading = document.querySelector(".heading"); const global = document.querySelector(".global_menu"); const local = document.querySelector(".local_menu"); const sidebar = document.querySelector(".sidebar"); const main = document.querySelector(".main_content"); heading.append(addTitle("React: Creating and Hosting a Full-Stack Site")); heading.append(addParagraph("Shaun Wassell - LinkedIn Learning - September 2022")); heading.append(addParagraph("Chapter 3 - Adding MongoDB to Node.js")); main.append(addHeader("WHY MONGODB?")); main.append(addParagraph("MongoDB is a non-relational database that works very well with Javascript and JSON and is possibly easier to use than a relational DB such as My SQL or Maria DB.")); main.append(addHeader("DOWNLOADING AND INSTALLING MONGODB")); main.append(addSyntax("/usr/bin")); main.append(addParagraph("For our blog, we created a folder in the my_blog_backend directory called mongo-db-data which will hold our data. We can start the mongo demon with the command")); main.append(addSyntax("mongod --dbpath ./mongo-db-data/")); main.append(addParagraph("The dbpath options tells mongo where data should be stored. You can run this in a terminal in VS Code but you will need to open a second terminal. Another option would be to have it running in a separate terminal or set it up so that it runs in the background on start up which you can do with the command")); main.append(addSyntax("sudo systemctl enable mongodb")); main.append(addParagraph("In a dev environment, it doesn't really matter too much so at this point, I am running it in a terminal in VS Code.")); main.append(addParagraph("In the second terminal, we can use the command")); main.append(addSyntax("mongo")); main.append(addParagraph("to start up an interactive session.")); main.append(addParagraph("Like My SQL, you can use the use command to work with a specific database but you don't need to create it first. For example, if we want to connect to the react-blog-db database, we would use the command")); main.append(addSyntax("use react-blog-db")); main.append(addParagraph("in the terminal and we should see the response")); main.append(addSyntax("switched to db react-blog-db")); main.append(addParagraph("We can insert our three existing mains using the db.mains.insertMany function as shown here.")); main.append(addImageWithCaption("./images/insert_many.jpg", "Inserting the mains into our database.")); main.append(addParagraph("This is a little bit different to inserting data into a relational database but the principle is the same. Instead of tables, we have collections and if you look at the insertMany command above, the name of the collection here is mains.")); main.append(addParagraph("This allows us to insert several documents into the database with a single command.")); main.append(addParagraph("We can retrieve documents from a collection using the find command so, for instance,")); main.append(addSyntax("db.mains.find({})")); main.append(addParagraph("will return all the documents in the collection as shown below.")); main.append(addImageWithCaption("./images/mongodb_find1.jpg", "Using the find command in mongodb.")); main.append(addParagraph("Notice that the curly braces are empty which is why we get all the documents being returned but we can also insert a specific what we want to search for in the collection. That might be something that returns a specific main such as the name, learn-react or we can use a non-unique property such as zero upvotes which will, for me at least, return all three mains. Examples are shown below with results.")); main.append(addImageWithCaption("./images/mongodb_find2.jpg", "Searching for something specific with the find command.")); main.append(addParagraph("So we now have mongodb set up to store the data for the backend, but we don't want users to log into the database in order to upvote an main or add a comment. The database should be updated when the user performs one of those actions in the front end. Next, we will look at adding mongo to express so that express can do that.")); main.append(addHeader("ADDING MONGODB TO EXPRESS")); main.append(addParagraph("Our next step is going to be to add mongodb to our project which we can do with the node command ")); main.append(addSyntax("npm install mongodb")); main.append(addParagraph("We also need to make some changes to server.js and we can start by deleting the mainsInfo array since the mains are now in our database.")); main.append(addParagraph("Our endpoints will also change because these will now get the main data from the database and will update that data or add new data as necessary.")); main.append(addParagraph("We will begig that process by creating a new endpoint to retrive an main from the database. We will use a URL parameter, just as we did before to get the main name which we can then use in our database query. The first line of that endpoint will be")); main.append(addSyntax("app.get('/api/mains/:name', (req.res) => {")); main.append(addParagraph("Next, we will get that name parameter and again this is exactly as we did it previously")); main.append(addSyntax("const { name } = reg.params;")); main.append(addParagraph("We are now going to connect to the database, but to do that, we will need to add an import statement to get MongoClient from mongodb")); main.append(addSyntax("import { MongoClient } from 'mongodb';")); main.append(addParagraph("and this is going to allow us to interact with the database from the client rather that the server side.")); main.append(addParagraph("We need to create an instance of MongoClient and we will pass it the URL of the database as follows")); main.append(addSyntax("const client = new MongoClient('mongodb://127.0.0.1:27017');")); main.append(addParagraph("So this is just connecting us to our localhost database but we do need to specify this an an ip address and we will be using port number 27017 (this is the default port number for mongodb).")); main.append(addParagraph("The command to connect to mongo is asynchronous and we are not really going to dig into what that means, just that it requires us to use await in the connection and also add async to our endpoint.")); main.append(addSyntax("await client.connect();")); main.append(addParagraph("is used to establish our connection and the endpoint becomes")); main.append(addSyntax("app.get('/api/mains/$name', async (req,res) => {")); main.append(addParagraph("We can get the database we want and also create a reference to it with a command such as")); main.append(addSyntax("const db=client.db('react-blog-db');")); main.append(addParagraph("This is the node equivalent of mongo's use command so we have connected to a specific database which we can reference with db.")); main.append(addParagraph("We can now run a query on the database to retrieve an main with")); main.append(addSyntax("const main = await.db.collection('mains').findOne({ name }) ;")); main.append(addParagraph("This is similar to the find command in mongo but there are a couple of differences. In mongo, we referenced a collection just with its name")); main.append(addSyntax("db.mains")); main.append(addParagraph("but here, it is slightly different")); main.append(addSyntax("db.collection('mains')")); main.append(addParagraph("and this is how we reference a collection in node.js. The findOne function can be used in mongo as well as node. When we search for an artide with a specific name, we only expect one main to be returned, but if there is more than one match, find would return all of the matching mains. As the name suggests, the findOne function will return one main regardless of how many matches it finds.")); main.append(addParagraph("The findOne function seems to be a little redundant in this case, but it is in fact commonly used when searching with a unique id such as the name in our mains.")); main.append(addParagraph("The last thing we need to do is to return the main. We have already seen several examples where a value was returned, but these have been string values. Now we are returning a JSON object so we need to change the syntax of this.")); main.append(addSyntax("res.json(main);")); main.append(addParagraph("This just makes sure that the response is correctly sent and has , for example, the right headers for JSON. At this point, we can test it by sending a Postman request with the result shown below.")); main.append(addImageWithCaption("./images/postman12.jpg", "Testing the functionality that returns an main from the database.")); main.append(addParagraph("As you can see, we got a JSON object (or a mongodb document) back and this is the same data we had previously stored in an array. In this case, we have removed the array so now this is coming from the database.")); main.append(addParagraph("One last thing we want to add is code to check whether the main has been found before we try to send it back so we will use the same if statement we used earlier which is")); main.append(addSyntax("if (main)")); main.append(addParagraph("and we will send the JSON response if this returns a true value and we will add an else clause to return a suitable response if it isn't . That response could be a string or we could send back the HTTP response code hich would be a 404 if the main isn't found. To send back a status we would use the sendStatus function rather than send.")); main.append(addSyntax("res.sendStatus(404);")); main.append(addParagraph("That would usually be enough, a 404 response is common enough that you might expect a user to recognize it.")); main.append(addParagraph("If you wanted to send both a status and a message, you would use both methods together like this.")); main.append(addSyntax("res.sendStatus(404).send('main not found!');")); main.append(addParagraph("Since we have changed the way in which the response is being sent, it is good practice to test again with an existing main to make sure we still get the same response and then we can check with random main name to make sure we get the correct response. I tested this with only the status code being returned with this result.")); main.append(addImageWithCaption("./images/postman13.jpg", "Testing for a 404 response.")); main.append(addParagraph("That seems to work correctly and notice that it does in fact return a Not Found message.")); main.append(addParagraph("However, I did notice that when I tried to send a response back with both the status and a message, I got exactly the same response so the sendStatus message is being sent correctly. The node server then cracher when he message is sent and this is shown below.")); main.append(addImageWithCaption("./images/status_error.jpg", "The error generated when trying to send a message with send after sending a status with sendStatus.")); main.append(addParagraph("The error message seems to suggest that the send method is trying to set the HTTP status. Interestingly, if you switch these two functions around, this doesn't work at all and it crashes with this error .")); main.append(addImageWithCaption("./images/status_error1.jpg", "The result generated when trying to send a status with sendStatus after a message sent with with send.")); main.append(addParagraph("This time, the sendStatus method is not recognised which suggests that either the methods can't be linked like this or the syntax is slightly off. Another possibility is that this is because of a difference in versions but Shawn didn't run this version in the course video so I don't know if he would also have seen an error.")); main.append(addParagraph("In any case, we can now retrieve an main from mongodb so we can start updating our upvote and comments endpoints to use that and we will start with upvote.")); main.append(addHeader("REWRITING YOUR UPVOTE ENDPOINT")); main.append(addParagraph("For the upvote endpoint, there are three steps.")); main.append(addParagraph("As such, this is fairly straightforward and for the first part, the process is pretty much as described in the previous section.")); main.append(addParagraph("We will start by using Mongoclient to connect to the database")); main.append(addSyntax("const client = new MongeClient('mongodb://127.0.0.1:27017);")); main.append(addParagraph("We will initiate the connection with")); main.append(addSyntax("await client.connect();")); main.append(addParagraph("and select the database and create a reference to it.")); main.append(addSyntax("const db = client.db('react-blog-db');")); main.append(addParagraph("So that is exactly the same.")); main.append(addParagraph("We don't need to get the main from the database, we got the name in the URL parameter so now, we just need to update the object in the database which has that name. The code to do that is")); main.append(addInsetCodeListing(["await db.collection('mains').updateOne({ name }, {", " $inc: { upvotes: 1 },", "})"])); main.append(addParagraph("Note that in order to implement the functionlity for upvotes, we didn't have to actually retrieve the main but when we output the success or fail message, we are going to test whether it is in the database so we will need to retrieve it before we do that we will use the same find One function.")); main.append(addSyntax("const main = await db.collection('mains').findOne({ name });")); main.append(addParagraph("We then have the same if statement which checks whether an main exists and if it does, it will send back an appropriate success message, if not it will send back a fail message. I will also change the else clause so that we are returning a 404 status.")); main.append(addParagraph("One thing that might be easy to miss is that we should remove the line that we previously used to increment upvotes, otherwise we might increment it twice! ")); main.append(addHeader("REWRITING YOUR COMMENTS ENDPOINT")); main.append(addParagraph("The comments endpoint is going to be very similar to the upvotes endpoint so I will start by copying the code to connect to the database and update the upvote counter and this will replace the find statement we already have as well as the push statement we used to update the array.")); main.append(addParagraph("The $inc function won't be used here but that function is the only thing we need to change. Everything else is the same.")); main.append(addParagraph("The mongodb command to add an element into an array in the database is also push so our $inc command becomes $push and the parameters will become")); main.append(addParagraph("We can also use the same find comment to get the main so we can confirm the main is valid and we can keep the same if...else statement but we will remove the statement used to update the array in memory that we are no longer using.")); main.append(addParagraph("The update db query is probably the most important part of the endpoint and it is shown below.")); main.append(addInsetCodeListing(["await db.collection('mains').updateOne({ name }, {", " $push: { comments: { postedBy, text } },", "})"])); main.append(addParagraph("That's all we need to do to rewrite the comments endpoint and we can test it. Since we are now saving the data in mongodb, we can now restart the server but the comments and upvotes are now persitent so they don't disappear when the server restarts.")); main.append(addParagraph("Now, you may have noticed that creating the endpoints was fairly quick and that was because we were able to copy and paste the code for connecting to the database and that sounds like it makes things easier for us . It does, in the short term.")); main.append(addParagraph("It can also cause problems because it means you have the same code in multiple places in the same application. Of course, in our case, they are even in the same file.")); main.append(addParagraph("Let's say the name of the database changes. We then have to update this wherever we are using that code to connect to the database. Even worse, we might switch to a different database which means we might have to rewrite the code and make sure we replace it wherever it is used.")); main.append(addParagraph("If we add, let's say, another endpoint, we then have to find the code, work out what we need in order to connect the new endpoint to the database and then copy it and insert it.")); main.append(addParagraph("So, we might say that copying and pasting code is bad for several reasons.")); main.append(addInsetCodeListing(["It gives you multiple copies of the same code which can mean making the same change in several parts of your code if there is a change.", "Finding the code and copying exactly what you need isn't always easy to do.", "It doesn't scale very well.", "It can lead to errors if you update the code inconsistently."])); main.append(addParagraph("A much better solution is to have just one copy of the code so if there is anything that needs to be changed, it only has to be changed in one place. It also gives you a separation of concerns because if you create a function to handle the database connection, it only has to be concerned with that connection. At the same time, your comment and upvote endpoints only have to be concerned with comments and upvotes. They can make use of the db connection to do what they need to do, but they don't have to know the details of the connection.")); main.append(addParagraph("We could write a function in the server.js file, but for maximum reusability, it would be better to keep that separate so we will create a separate file inside the src folder which we will call db.js. Inside that file we will create a variable which will be used as a reference to the database and we will then export the function and the variable.")); main.append(addParagraph("We have already seen the code we need to connect to the database and in terms of the functionality, this is not any different. So any changes we are making to the code here is just to accomodate the fact that we are going to connect using a reusable component in another file. Let's start by looking at the code in db.js.")); main.append(addInsetCodeListing(["import { MongoClient } from 'mongodb';", "", "let db;", "", "async function connectToDb(cb) {", " const client = new MongoClient('mongodb://127.0.0.1:27017');", " await client.connect();", " db = client.db('react-blog-db');", " cb();", "}", "", "export {", " db,", " connectToDb,", "}"])); main.append(addParagraph("If we compare this to the code from, let's say the comments endpoint, there are three obvious differences. The first of these is the variable db. Second is the fact that our connection code is now inside a function rather than inside an endpoint. Last, we are exporting the database.")); main.append(addParagraph("The db variable is pretty similar, as with the comments endpoint, we will need to be able to reference the variable and we do make use of it in the connectToDb function in more or less the same way.")); main.append(addParagraph("In the function itself, we are passing cb to it and at the end of the function, we have a call to cb. I'm not sure if the course explains what this is but for now, we will simply accept it as a requirement. I would guess that we need this because our endpoint is going to call this function from an anonymous callback function so this passed a cb object over to connectToDb and the cb() call is to confirm to the callback function that processing is finished and return values are ready to be passed over.")); main.append(addParagraph("The export function is passing db back because that gives the function access to the database connection that connectToDb has established and exporting the function gives access to it outside of the db.js file . In our case, this will mean that we can call it from the server.js file.")); main.append(addParagraph("In the server.js file, we can remove the line that imports MongoClient (which is now in db.js) and we will want to import db and connectToDb.")); main.append(addSyntax("import { db, connectToDb } from './db.js';")); main.append(addParagraph("Note that the js extension for d b is required and this is because of the fact that we set type to module in the package json file.")); main.append(addParagraph("We can delete the database connection code from each of our endpoints so that is ")); main.append(addInsetCodeListing(["const client = new MongoClient('mongodb://127.0.0.1:27017');", "await client.connect();", "", "const db = client.db('react-blog-db');"])); main.append(addParagraph("These lines, of course, are the lines of code in the connectToDb function. We still have a reference to db because we do still need to use it but we have removed the code we previously used to create it because our function will now create it for us.")); main.append(addParagraph("So now we just have to call that function but we won't call it from within the endpoints . We will call it when the server starts up so it is always available when the server is running.")); main.append(addParagraph("You might recall that we set the server to listen on port 8000 with app.listen so what we will do now is call connect ToDb and attach a callback function to it that holds that code.")); main.append(addInsetCodeListing(["connectToDb(() => {", " console.log('Successfully connected to the database!')", " app.listen(8000, () => {", " console.log('Server is listening on port 8000');", " });", "});"])); main.append(addParagraph("Now, when we run the server it will connect to the database and then start listening on port 8000 and we are logging messages to the console for both events. In the image below, you can see the call to connectToDb but I started the server before saving the file. Saving the file restarted the server and you can see output has changed and is now telling Les that the connection to the database has been made.")); main.append(addImageWithCaption("./images/connect_to_db1.jpg", "Starting the server automatically connects us to the database.")); main.append(addParagraph("It is important to remember that these changes are to the implementation, not the functionality so nothing should change from the users perspective so we can run our tests in Postman and the results of one of these tests is shown below.")); main.append(addImageWithCaption("./images/connect_to_db.jpg", "Retesting the endpoints in Postman.")); main.append(addParagraph("Notice that we are still showing comments added in previous tests which demonstrates a couple of points. Firstly, it shows that our data is persistent so the database is doing its job. More importantly in this context, it shows that after changing the way we access the database, the data it previously held is still there so the change is completely invisible to the end user.")); main.append(addParagraph("Now that our app has been completely rewritten to use mongodb rather than temporary storage, the backend is complete and we can now move on to connecting the front end to the backend.")); addSidebar("webdev");