You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

175 lines
26 KiB

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 2 - Creating a Node.js Back End"));
main.append(addHeader("WHY Node.js?"));
main.append(addParagraph("This is an important question because there are a large number of competing technologies in web development, but that is really beyond the scope of this course. However, in the context of a course in React, it makes sense to say that the overriding advantage of nodejs is that it allows us to write the backend and the front end in the same language."));
main.append(addParagraph("Before node.js was introduced, backend development was done in PHP or some other programming lanaquage so a full stack developer had to be proficient in at least 2 languages, but now, everything can be done in JavaScript."));
main.append(addHeader("SETTING UP AN EXPRESS SERVER"));
main.append(addParagraph("As with node.js, you might also ask why Express server which is possibly a more interesting question because of the number of alternatives. Again we won't really explore the details in this course, but if you are new to backend development, as I am, it's a good starting point because it's reasonably easy to learn, there are plenty of third-party add-ons available and the online documentation is pretty good. Learning Express will allow you to move on to competing technologies better able to understand its advantages and disadvantages."));
main.append(addParagraph("The first step to setting up the backend will be to create a folder for it. This is separate from the front end so we won't put it in the same folder we used previously (my_blog) but we will put it in the same folder that contains my_blog. In my case, that is a folder called React which is in my Projects folder in my home directory."));
main.append(addParagraph("We will call the new folder my blog backend and we will open it up in Visual Studio code. There is a short cut to doing that from the terminal, from the React folder, we can type"));
main.append(addSyntax("code my_blog_backend"));
main.append(addParagraph("and this will open up VS Code with our backend project being displayed. At this point, the folder is empty so we will start by setting up a new package with the command"));
main.append(addSyntax("npm init -y"));
main.append(addParagraph("Note that when we are developing the backend, you can assume that unless a different directory is mentioned, any command from the terminal will be executed from inside the backend folder, my-blog-backend."));
main.append(addParagraph("The init command generates our package.json file and we can then install express with"));
main.append(addSyntax("npm install express"));
main.append(addParagraph("For our source code, we will create a folder called src. Remember that to some extent, Visual Studio Code, or indeed any IDE acts as a file manager in that it provides you with a visual representation of the files in your project and the file structure so you can create the src directory either directly in VS code or from the terminal."));
main.append(addParagraph("In the src folder, we will create a file for our server code and we will call it server.js. Before we write any code, we need to make sure our project can handle modern Javascript and in the past, we would do that by configuring a Javascript transpiler like Babel, but it's a little easier now."));
main.append(addParagraph("We just need to add the line"));
main.append(addSyntax("\"type\":\"module\","));
main.append(addParagraph("to our package.json file and we will put that right after the lime that starts \"main\". It's not the last line in the file so the comma at the end is important."));
main.append(addParagraph("To start with, we will give our server a very basic structure that will accept a request and return some message and since we are using express to do that, we will import it into our server.js file."));
main.append(addSyntax("import express from \"express\";"));
main.append(addParagraph("We can create an express app simply by creating a const called app and setting it's value to the express function like this."));
main.append(addSyntax("const app = express();"));
main.append(addParagraph("Setting up the server to do something useful will then involve creating endpoints and defining what happens when an end point receives a request. To do that we will define a function corresponding to some http method so it will look like this for the get method."));
main.append(addSyntax("app.get()"));
main.append(addParagraph("We will insert two arguments into that get function, the first being the route and the second being a callback function which is called when a get request is sent to that endpoint. The callback function has two main arguments, a request object and a response object. The request, as you could probably guess, contains information relating to the request and the response is used to send back a response. It is actually an instance of the response class so it allows us to use the response methods."));
main.append(addParagraph("The goal for our first endpoint is to set it up with the route"));
main.append(addSyntax("/hello"));
main.append(addParagraph("and send the word Hello back as a response. The image below shows the server.js file with the endpoint set up."));
main.append(addImageWithCaption("./images/server_js1.jpg ", "The server.js file with a simple endpoint configured."));
main.append(addParagraph("Notice that the response method we used is send which simply sends a message back to the request source. We have also added a listener which specifies the port that our server will listen on and this also has a callback function that runs when the server is running (in other words when it is listening on pen 8000) and we use this to log a message to that effect to the terminal."));
main.append(addParagraph("So this isn't particularly useful but it demonstrates some principles which we can expand on in order to perform some functionality that is really useful. For now, we will run our express server with the command"));
main.append(addSyntax("node src/server.js"));
main.append(addParagraph("We should see the listeners message appearing in the console and in a browser, we can send a request to the server via the URL."));
main.append(addSyntax("localhost:8000/hello"));
main.append(addParagraph("and we should see the servers response like this."));
main.append(addImageWithCaption("./images/hello.jpg", "The response from the server."));
main.append(addHeader("TESTING AN EXPRESS SERVER WITH POSTMAN"));
main.append(addParagraph("There are a number of ways to test our server and the most basic of these is to test the URL in a browser as we have already done. However, there are other REST clients you can use for testing, including VS code if you have installed the REST client extension."));
main.append(addParagraph("The following image shows a test of the server in Firefox."));
main.append(addImageWithCaption("./images/browser1.jpg", "Testing the express server in Firefox."));
main.append(addParagraph("Notice that I used the ip address of my dev machine rather than localhost because I am testing it from my Windows machine. This is on the same network so we can access the server using that ip address."));
main.append(addParagraph("In my experience , testing with Visual Studio code can be a little bit awkward, but the image below shows a request in Visual Studio code."));
main.append(addImageWithCaption("./images/vscode2.jpg", "Running a test in Visual Studio code."));
main.append(addParagraph("The best way to test the server is to use a dedicated tool and we will be using Postman, as shown below."));
main.append(addImageWithCaption("./images/postman1.jpg", "Testing our server in Postman."));
main.append(addParagraph("Postman is quite flexible. For example , we can easily change the GET request to a POST request by simply selecting the POST method, you might remember that we implemented a GET method in our server app, so it probably wouldn't surprise you to know that if we we change the method to POST without making any other changes, we will see an error as shown below."));
main.append(addImageWithCaption("./images/postman2.jpg", "Error - cannot POST /hello"));
main.append(addParagraph("This is telling us that there is no POST request on that endpoint and you might have noticed that the HTTP status that was returned in 404."));
main.append(addParagraph("In express, it is easy to tell if an endpoint uses the GET or POST method (or indeed any other method) and we can easily convert ours from GET to POST by changing this in the server code. If we do that, we will have to restart the server for the changes to take effect and now, we get the message returned when we send a post request."));
main.append(addParagraph("image"));
main.append(addParagraph("Since we changed the endpoint from GET to POST, we will now get an error if we send this as a GET request."));
main.append(addParagraph("This works because the POST method can do the same things that the GET method can do, but unlike GET it also allows us to send a body or payload with or request. For example, the image below shows a JSON payload and the result we get when we send the request with that pay load."));
main.append(addImageWithCaption("./images/postman4.jpg ", " A POST request with a JSON payload."));
main.append(addParagraph("Since the endpoint isn't actually doing anything with the payload the output is still the same, but the body is being sent to the server so it can access it . It is sent with the request, it's actually a property of the request so it can be accessed with"));
main.append(addSyntax("req.body"));
main.append(addParagraph("To demonstrate this, we will add the line"));
main.append(addSyntax("console.log (req.body)"));
main.append(addParagraph("to the server's POST method and then restart the server and send that POST request again. We don't see anything different in Postman, but in the terminal, we see undefined, as shown here."));
main.append(addImageWithCaption("./images/undefined.jpg", "The output we see in the console when logging the request body."));
main.append(addParagraph("The reason we see undefined in the output is that express doesn't parse the JSON in the body by default so we have to tell it to do that by adding the line"));
main.append(addParagraph("to the server.js file. This allows the JSON to be parsed automatically which means that it is available to be accessed as the request body so when we save, restart the browser and send the post request again, we now see the body being logged to the console as shown below."));
main.append(addImageWithCaption("./images/body.jpg", "Logging the request body to the console."));
main.append(addParagraph("So that demonstrates the fact that we can access the body."));
main.append(addParagraph("We are still not making any use of it, however, so we will change that by amending the message that is being returned so that rather than just sending Hello back, it sends a personalised greeting using the name in the body. To do this, we will change our call to the send method to"));
main.append(addSyntax("res.send (\`Hello, ${req.body.name}`);"));
main.append(addParagraph("This is pretty straight forward. We have the string holding the message to be returned surrounded by back ticks and you may recall we do that to allow us to insert a variable name into the string. The variable is the name property in the body and the body is a property of the request, so the variable name is"));
main.append(addSyntax("${reg.body.name}"));
main.append(addParagraph("Again, we will save this, restart the server and send the Postman request again."));
main.append(addImageWithCaption("./images/postman5.jpg", "Using the request body to personalize the response."));
main.append(addParagraph("As you can see, the response now also returns the name sent in the body of the POST request."));
main.append(addHeader("URL PARAMETERS IN EXPRESS"));
main.append(addParagraph("When we developed the front end, we used a URL parameter to identify the main that we wanted to view and this is similar to using a request body in the sense that it is being used to send a little bit of extra data to the server."));
main.append(addParagraph("With a request body, it is easier to send more complex information, but our server can also use these URL parameters and sometimes, that may be the best option."));
main.append(addParagraph("For example, we accessed a specific main with something like"));
main.append(addSyntax("local host:3000/learn-react"));
main.append(addParagraph("So the URL paramenter is learn-react. Let's say that we want to add some functionality allowing us to upvote our favourite mains, we can use the URL parameter to identify the main and possibly append/upvote on the end of it to derote the required action."));
main.append(addParagraph("To demonstrate this, we will add another GET method which will use a URL parameter so a user can call this method using a name as the parameter in order to generate a personalized greeting. The first line of this will look pretty similar to our POST method."));
main.append(addSyntax("app.get('/hello/:name', (reg,res) => {"));
main.append(addParagraph("where name is going to hold the URL parameter. In the next line"));
main.append(addParagraph("we are getting the URL parameters and using them (or it, since We are only expecting one) to initialize the const value name. We can then use that const to generate the message."));
main.append(addSyntax("res.send(`Hello ${name}`)"));
main.append(addParagraph("Notice that the syntax is a bit cleaner than the corresponding syntax in the POST method . We have also used back ticks again so we can include a variable (actually a const but the principle is the same) in the output string."));
main.append(addParagraph("If we save this, restart the server and send a GET request using the endpoint"));
main.append(addSyntax("192.168.010:8000/hello/Philip"));
main.append(addParagraph("we get this output."));
main.append(addImageWithCaption("./images/postman6.jpg", "The output when sending a GET request with a URL parameter."));
main.append(addParagraph("You may have noticed that I skipped the comma in the response because we now have both a POST and a GET method and they both do the same thing, the only difference being the way in which the name is passed to the server . Having a minor difference in the output of the two methods means that we can be certain that the right method was used to generate the response."));
main.append(addHeader("UPVOTING mainS"));
main.append(addParagraph("We are now going to start adding some code to our server that will provide additionel functionality to our blog, specificially the ability to upvote an main."));
main.append(addParagraph("To do that in such a way that the votes are persistent rather than just existing while the server is running, we will need to store the date in a database and we will do that later. For now we just want to get the logic working so we will create an array of JSON objects which will act as a temporary database."));
main.append(addParagraph("Each of these mains will look something like this"));
main.append(addParagraph("and we will have one for each of our three mains."));
main.append(addParagraph("We can delete our existing POST and GET methods because we don't need those for our blog. We will replace them with a PUT request and we will use PUT as the method because we want to update some information on the server."));
main.append(addParagraph("Our endpoint will be"));
main.append(addSyntax("/api/mains/:name/upvote"));
main.append(addParagraph("The /api/ part of the endpoint is something we haven't seen before but we will come back to that later."));
main.append(addParagraph("Remember that the goal is to upvote an main, so the first thing we will need to do is to work out which of the mains that is and we will create a const so we can get and use the URL parameters."));
main.append(addSyntax("const { name } = req.params;"));
main.append(addParagraph("Next, we are going to use the built-in find function in JavaScript to locate the correct main and we will create a const called main to hold it."));
main.append(addSyntax("const main = mainsInfo.find( a => a.name === name);"));
main.append(addParagraph("We want to use an if statement to make sure the main exists"));
main.append(addSyntax("if (main) {"));
main.append(addParagraph("and if it does, we will do two things . Firstly, we will increment the upvotes counter for that main."));
main.append(addSyntax("main.upvotes += 1;"));
main.append(addParagraph("Then we will send a message back to the sender to confirm the new value for upvotes on that main."));
main.append(addSyntax("res.send(`The ${name} main now has ${main.upvotes} upvotes`);"));
main.append(addParagraph("To handle a situation where the main is not found, we will add an else clause to our if statement and send back a message to let the user know"));
main.append(addSyntax("res.send('That main doesn\'t exist');"));
main.append(addParagraph("We can now save the file, restart and create a new request in Postman with PUT as the method and"));
main.append(addSyntax("localhost:8000/api/learn-react/upvote"));
main.append(addParagraph("as the endpoint and if we send that, we will get the result shown below ."));
main.append(addImageWithCaption("./images/postman7.jpg", "Testing the upvotes functionality in Postman."));
main.append(addParagraph("We can also amend the endpoint so that it doesn't match any of the existing messages to demonstrate that this returns a message confirming the chosen main doesn't exist."));
main.append(addParagraph("To finish off here, it might be worth taking a look at what the server.js file now looks like."));
main.append(addImageWithCaption("./images/server_js2.jpg", "The server.js file with upvotes functionality."));
main.append(addHeader("AUTOMATICALLY UPDATING WITH NODEMON"));
main.append(addParagraph("Every time we make a change to our code, we are also restarting the server before the changes can take effect. However, node provides a package that will do that for you and that is nodemon. The command to install that is"));
main.append(addSyntax("npm install nodemon --save-dev"));
main.append(addParagraph("We use the save-dev option so that nodemon will be installed as a dev dependency. The reason for this is that we will only use it in development, when running an app in production, we will either use a more suitable package or we will just use node."));
main.append(addParagraph("We can then run the server with the command"));
main.append(addSyntax("npx nodemon src/server.js"));
main.append(addParagraph("When it starts the server, nodemon provides some output to confirm that it is watching for changes. If we now make some change to the code, for example by adding a few exclamation marks to the end of our upvotes comment, once the changes have been saved we can just go ahead and test that in Postman."));
main.append(addImageWithCaption("./images/postman8.jpg", "Testing changes to the code without re-starting the server."));
main.append(addParagraph("As you can see, the changes we made are reflected in the output without our having to restart the server. If we look at the terminal, we can see some addition output from nodemon showing that it detected the changes and that it did, indeed, restart the server."));
main.append(addImageWithCaption("./images/nodemon1.jpg", "The output from nodemon confirming that it restarted the server."));
main.append(addParagraph("We can make it a little bit easier to run the server with nodemon by creating essentially the node equivalent to an alias which we do by adding a script to the package.json file which we will call dev. In the package.json file we will add this line to the script section."));
main.append(addSyntax("\"dev\": \"nodemman src/server.js\","));
main.append(addParagraph("Notice that we can drop npx since this is being run from the package.json file and if it is not the last script, we need to ensure there is a comma at the end to act as a seperator. If it is the last script, you will probably need to add a comme at the end of the previous line or you could get into the habit of always having a separator for the last item. This allows you to add an item at any time without worrying about editing existing lines."));
main.append(addParagraph("Just a sidenote and this should be fairly obvious, when the package.json file is saved, it isn't making any changes to the functionality of the app but nodemon does detect the change and does restart the server."));
main.append(addParagraph("We will stop it now and restart it with the script by entering the command"));
main.append(addSyntax("npm run dev"));
main.append(addParagraph("and we will test it to make sure it is still working correctly by removing those exclamation marks from the upvates comment and resending the request."));
main.append(addImageWithCaption("./images/postman9.jpg", "Testing to make sure that the command npm run dev works as expected."));
main.append(addParagraph("We can see here that the exclamation marks have disappeared so the server was restarted correctly. We can also see that in the terminal."));
main.append(addImageWithCaption("./images/nodemon1.jpg", "Confirmation that the server is still being successfully restarted."));
main.append(addParagraph("This also shows a comparison of the two different ways of running nodemon and you can see that aside from a couple of additional lines added because we are running this from a script, they both work in the same way."));
main.append(addParagraph("Another thing you might have noticed is that in both cases, the output in Postman shows 1 upvote for the main we ran the test with, learn-react . This is, of course, because the data is not yet persistent so every time the server restarts, our data is initialized again and every main has its upvotes counter reset to zero."));
main.append(addHeader("ADDING COMMENTS"));
main.append(addParagraph("Adding comments is not really all that different from adding upvotes. We need to add something to the data we have for each main that will hold the comments and we will also add an endpoint that is going to actually add a comment."));
main.append(addParagraph("We'll starting by adding an array like this."));
main.append(addSyntax("comments: [],"));
main.append(addParagraph("The endpoint will have some similarities to the one we set up for upvotes so a good place to start on that would be to copy and paste that upvotes endpoint."));
main.append(addParagraph("The endpoint can be either a PUT or a POST request. The distinction is that PUT is intended to be used to update day on the server (as we did with upvotes where we updated the contents of the upvotes variable)."));
main.append(addParagraph("With our comments, you could argue that we are updating the contents of the comments array or that we are adding new elements to that array. Either way, both methods would work so you could choose whichever you think is more appropriate here."));
main.append(addParagraph("Another approach might be to only use one or the other but it would make our API a little more user friendly if you do choose the methed consistently!"));
main.append(addParagraph("We need to decide what format our comments will take and perhaps the easiest way to do that is to compose a Postman request to add a comment - that will force us to think about what data we want to store in each comment and how to represent that in our code."));
main.append(addParagraph("In Postman, we will change the method to POST since adding data seems to be more appropriate. The easiest way to think of this is that from a user's perspective, they are adding rather than updating the comment and how this is handled by the backend isn't really relevant."));
main.append(addParagraph("We will also change the endpoint and add a body. We want to add a name so we can see who posted the comment and the comment itself so we will represent these with \"Posted By\" and \"text\" in the JSON so a typical request might look something like this."));
main.append(addImageWithCaption("./images/postman10.jpg", "The POST request to add a comment."));
main.append(addParagraph("In the callback function, we set the endpoint and then we get the data from the request like this."));
main.append(addSyntax("const {postedBy, text} = req.body;"));
main.append(addParagraph("We will need to get the main name and the method we use is exactly the same as the one used in our upvotes endpoint."));
main.append(addParagraph("Aside from petting the data from the request, the only difference betweeen this callback function and the one attached to the upvotes endpoint is that we will replace the code that increments the upvotes for an main with code that will add the comment to the appropriate comments array. We am use the same if...else strutture to check whether the main exists and add the comment if it does."));
main.append(addParagraph("To actually update the comment, we will use the push method to de an element to the array like this."));
main.append(addSyntax("main.comments.push({postedBy, text});"));
main.append(addParagraph("Of course, we should also change the message that is sent back to the user and we want to do that in a way that demonstrates that the request was successful. The easiest way to do that is to simply return the entire comments array showing the new comment."));
main.append(addParagraph("So now we can go ahead and test it in Postman."));
main.append(addImageWithCaption("./images/postman11.jpg", "Testing the add comments functionality with Postman."));
main.append(addParagraph("Here, you can see the comment being added and this is shown as the last comment in the returned array. Notice that this was the second comment added, so we can see both of these comments in the output."));
main.append(addParagraph("It might just be worth adding a reminder here that we are not storing the data for our mains yet so any comments we add in testing will disappear when the server stops running or when it is restarted."));
addSidebar("webdev");