Why Learn Node.js Today?
One of the most valuable skills to have as a full stack web developer is without a doubt Node.js, and that's not going to change anytime soon. But you might be wondering why I'm making a Node tutorial in 2020, because if you've been watching the news you may have heard that Node was killed last week by Deno.
When I lay my vengeance upon them... making the extremely chaotic JavaScript world even more convoluted. If you're just starting to learn JavaScript this can be very confusing and you might be wondering, should I learn Node or Deno? The answer is Node, especially if you want to get a job or build a product. When something becomes as established as Node, it doesn't get replaced. You'll see things like this happen all the time. A few years ago everybody was saying GraphQL would replace REST, and 10 years ago people were saying Node.js would replace PHP. These predictions never pan out and are just built on top of hype. That being said, I do think Deno is awesome and has a bright future.
Both? Both. Both is good.
And if you learn Node today, those skills will mostly transfer to Deno tomorrow. In today's video, you'll learn the basics of Node.js in seven easy steps. Then we'll put those steps together to build a full stack web application. Because in my opinion, the best way to learn how to code is to build something meaningful from scratch. By the end of the video, we'll deploy our code to the cloud so you can show off your API to your friends and family. If you're new here, like and subscribe and head over to fireship.io to follow along with the full write up to this video.
Step 1: What Is Node.js?
Step one is to understand what Node.js is and why you would want to use it. Node.js itself is not a programming language but rather a runtime that allows you to run JavaScript on a server. You see, when JavaScript first came around in the 1990s, it was designed as a simple scripting language to run in the browser. As the web platform evolved, JavaScript became more and more powerful, and in 2009 we saw the initial release of Node.js. Now up until this time, it was impossible to write JavaScript code on the server. Most servers were written in languages like Java or PHP. It revolutionized web development because now a web developer could write a full stack application in a single language.
So that's all Node.js is, it's a way for developers to write JavaScript on the server when it could previously only be written in a web browser. Now, there are a lot of low-level implementation details here that you don't need to know about as a beginner. But if you really want to dive into how Node.js works, check out my How It's Made video. But what kind of problems can Node.js actually solve?
Node.js can do a lot, so instead of listing out a bunch of use cases, I'll show you how to build a full stack web application from scratch. This is how it'll work. You visit a URL on the internet that points to your server. When the request is received, we can use Node to handle the request and read a file from the server's file system, and then respond back to the client so they can view the HTML in the browser.
Step 2: Installing Node.js
Step two, how to install node.js. Node can be installed on Windows, Mac, or Linux and is probably already installed on your system. In this tutorial I'll be using Ubuntu via the Windows Subsystem for Linux. At this point I've just opened up an empty directory in VS Code and then pulled up a terminal session. You can find out if Node is installed on your system by running node -v from the command line. As you can see here, I'm running version 12.16, which is the current long-term support version. If you get an error at this point, it means that you don't have Node installed. And even if you do have Node installed, it'd be a very good idea to use a package called Node Version Manager to manage the installation of different Node versions on your system. There's an NVM package for Mac and Linux or a separate package for Windows. Go ahead and follow the instructions to install NVM on your system, and that will give you the ability to install any version of Node.js that you want. For example, you could run nvm install 12.16.3 then run nvm use to use that specific version as your runtime.
Step 3: "Hello, World!" in Node.js
And now that we have node.js installed, we can move on to step three, hello world. But first, if you're serious about node.js, consider becoming a pro member at Fireship.io. It'll give you access to my full length courses, almost all of which cover advanced use cases for nodejs. But we need to walk before we can run, so how do we actually use this thing? One way to mess around with Node is in REPL mode, which stands for Read-Eval-Print-Loop. When you type node into the command line, it allows you to run JavaScript code and will print out the results. For example, we can type console.log('hello world'), and it will log that value out. This is nice for messing around, but go ahead and hit Control+C twice to shut it down, because in most cases we want to execute JavaScript code that lives in an actual file. The default entry point into a Node.js app is the index.js file. Go ahead and create an index.js file manually in VS Code, then inside that file add console.log('hello world'). You can now run the code inside this file with the node command followed by the path to this file, and because it's an index file, you can actually just point to the parent directory. Congratulations, you just built a Node app.
Step 4: Understanding the Node Runtime & Globals
Step four, understand the node runtime. In most ways, JavaScript works the same way it does in the browser as it does in Node.js, but there are some very important differences you should know about. First, Node has a handful of built-in identifiers. One of those globals is console, which we've already been using to log out values to the terminal. There's another global with a name of global. This object is a namespace that is available throughout the entire node process. For example, if we assigned a lucky number property to global, we could then access it anywhere else in our code. If you're a front-end web developer, you can compare global to the window object in the browser. Now, I think the most important global to be familiar with is process, which gives you access to the currently running node process. You might use it to check the current platform or operating system, which in my case is Linux, or grab an environment variable from your server. But this gives us a great opportunity to segue into step five.
Step 5: How Events Work in Node.js
How do events work in node.js? You'll often hear people describe it as an asynchronous event-driven JavaScript runtime. The runtime implements a thing called an event loop, just like a web browser does, and it allows Node to push intensive operations off to a separate thread so only very fast, non-blocking operations happen on the main thread. And this is why people often call node non-blocking. It's a design choice that makes Node.js very suitable for things that require high throughput, like real-time applications, web servers, and things like that. Okay, so how does that affect me as a coder? Well, normally you won't have to think about the low-level details, you just need to know how events and callbacks work. In most cases, you'll listen to events. Events can come in many forms, but one example is on the process global that we looked at earlier. Before a Node process finishes, it emits an event named exit. We can listen to this event using on and then register a callback function as the second argument. When the exit event occurs, Node.js will run this function, and that's where it gets the name callback, because the function isn't called the first time Node.js sees it, it's only called after the exit event occurs at some point in the future. This event is built into Node, but you can also create your own from scratch. We'll import an EventEmitter from the events module that is built into node, and we'll look at modules in more detail in just a minute. We can create a custom event emitter by instantiating the class, and then we'll register a callback that fires on the lunch event. Now that the callback is registered, you can simply call eventEmitter.emit with that event name, and that triggers your callback function to run. As you can see here, we emit the event twice, which will run the callback function twice. This event-driven style of programming is extremely useful when you have something that is CPU intensive. And with that, we can move on to step six.
Step 6: Working with the File System (fs)
The file system. Node has a built-in file system module called fs. It can read, write, and delete files on the file system, among other things, and it can also do things in a blocking or non-blocking way. Allow me to explain that by showing you an example. First, create a file on the file system called hello.txt and then add whatever text you want inside of it. In our JavaScript code, we'll import two functions from the file system module called readFile and readFileSync. Anytime you see a function that ends in Sync, think blocking, or in other words, it will need to finish all of its work before any of your other code can run. We can read a text file in node by simply passing the path to that file and we'll specify the encoding as utf8. Now reading a file might take a while, especially if it's a very large file, and what you'll notice here when we run our code is that the second console log won't run until after that file has been read. Luckily you can make your code non-blocking by refactoring this to a callback with readFile. We pass the same first two arguments and then add a callback function as the third argument. Inside the function, we can access an error object if the operation fails, or when successful, the actual text from the file. The super cool thing about this is that even though the console log to the text file comes first in our script, it's not the thing that gets executed first. Node registers the callback, executes the rest of the script, and then finally runs the callback when the file has been read. So that gives us two different ways to read a file, but there's actually one other way we could go about this, and that's using a promise-based solution. Promises are also asynchronous and non-blocking and they tend to produce much cleaner code when compared to callbacks. Notice how in this example we're importing file from the promises namespace. This gives us a function that returns a promise when called. If you're using the latest bleeding edge version of node, you could potentially use this function with top-level await, which means you can use await to resolve the promise here at the top of the script. However, since we're currently using node version 12, we'll go ahead and wrap this in an async function. And this async/await syntax will make your code much easier to read, especially when you have multiple async calls in the same function. Okay, so now that you know how to read files, I want to backtrack a little bit and talk about this require function that you've seen me use in the past couple examples.
Step 7: Modules, NPM, and Third-Party Code
Step seven, modules. A module is nothing more than a JavaScript file that exports its code. Node has a bunch of built-in modules like fs and events that we've already looked at, and there's a long list of other modules beyond that. The traditional way to import a module in Node is to use this require function, but a quick side note on that first, because Node recently added support for ES modules, which use the import/export syntax. Most Node.js code out there written in vanilla JavaScript still uses require, so it's very important for you to know as a Node.js developer. And if all that's confusing to you, well, all I can say is, welcome to the JavaScript world.
Let's look at how we can use modules in our own codebase. Create a new file to serve as your module, then go into your index.js file, create a variable for the module and then import it using require. When you console log it, you'll notice that it's currently just an empty object. In order to make a module useful, you need to export some code from it. In the module's file, you can reference this object with module.exports. You can add new properties to the object or redefine it as a new object. In either case, whatever you add here will now be available to use in the other file. And as you can see, the object is no longer empty when we console log it.
But at some point, you'll very likely want to use somebody else's code out there in the world, and the primary place to do that is via Node Package Manager or NPM, which was recently acquired by GitHub, which itself was recently acquired by Microsoft. When you installed Node earlier in the video, it also installed NPM, which is a tool you can use to install remote packages to use in your own code. The first step we'll want to take is to open the command line and run npm init. We'll use the -y flag to use the default options, and what you'll notice is that it creates a package.json file here in the root of the application. This file can contain metadata about your project, but most importantly, it keeps track of the dependencies that you use here. At this point, we haven't installed anything yet, so let's go ahead and change that by opening the command line again and running npm install express. Express is a minimal web application framework and one of the most popular third-party Node modules. After running the command, you'll notice a few things happened here. In the package.json file, it added Express to the dependencies and pegged it to a specific version. This dependencies object allows you to manage multiple dependencies in a project and then reinstall them all at once on a different system. Now the actual raw source code for the dependency lives in this node_modules directory. You should never have to modify code in the node_modules directory. If you find yourself writing code in here, you're probably doing it wrong. That's because the package.json controls how this directory is built. It fetches your remote packages, saves the source here, and that process should be able to be repeated on any system. Now that we have this package installed, we can import it by name in our JavaScript code. In this case, we simply require('express').
Building a Full-Stack App with Express
And now we've reached the point where we're ready to put these seven steps together to build a real full-stack application and deploy it to the cloud. What we're building here is actually just your typical full-stack web application. Our server will live on a URL, and when a user makes a request to this URL in the browser, the server will respond with some HTML. In our code, we'll first create an instance of an Express app. An Express app allows us to create different URLs and endpoints that a user can navigate to in the browser. And then we define code for the server to handle those requests. Now, I don't want to get too deep into HTTP, but when the user navigates to a URL in the browser, it's what's known as a GET request, which means they're requesting some data on the server and not trying to modify or update anything on the server. With Express, we can set up an endpoint like that by calling app.get, and then the first argument is the URL that the user will navigate to. In this case, we just use a forward slash for the root URL, but feel free to create multiple pages for your web app by creating different URLs. The second argument here is our callback function. You can think of every request to this URL as an event, and then you handle that event with this function. Express gives us two parameters to make use of: the request and the response. The request is the incoming data from the user. In this example, we don't really need to parse any data from the request. However, in many cases, you might want to look at the headers or the body of the request to authenticate a user or understand what the user is trying to do. At this point, we need to implement the code to handle the request. What we want to do is read some HTML from our file system and then send it back down to the browser. As you can see here, I'm creating a file called home.html with just some generic HTML markup inside. In our source code, we can then import readFile from Node's file system module, just like we did earlier. We'll read the file, use utf8 encoding, then in our callback function here, we'll have access to the HTML string and we can send a response back down to the client by calling response.send. And also if there's an error in the callback, we can handle that by sending a response with a status code of 500, which means a server error. So the user knows that something went wrong on the server. And that's all there is to it. We now have a way to send HTML from the server to the client. Now we just need to tell our Express app to start listening to incoming requests. We do that by defining a port, which will normally come from a Node environment variable. Then we call app.listen with that port, and when it starts up, we console log that the app is available on localhost:3000. You can start it up by opening the command line and running Node with the current working directory. If you go ahead and open it in the browser, you should see your HTML returned back to you.
Now there is one important thing you should know at this point, and that's that callbacks can be very difficult to work with, especially as your app grows in complexity. It often leads to a state known as callback hell where you have a bunch of callbacks nested within callbacks within more callbacks, and so on. A great way to avoid code like this is to use promises instead of callbacks. And that's very easy to do in Node.js. Instead of importing readFile from fs, we'll import it from fs.promises. We can make our callback function async and then we can write the response in a single line of code by saying response.send and then await the operation to readFile. That's much more concise and readable, but it's especially useful when you have multiple async operations to handle in a single request.
Deploying Your Application to the Cloud
So now that we've built a Node.js app, how do we deploy it to the cloud so people can actually use it? There's a bunch of different ways we could do it, but an easy and free way is with Google App Engine. App Engine has what's known as a standard environment for node.js up to version 12, and what that does is provide you with a server in the cloud that scales automatically based on the incoming traffic to your app. It's incredibly easy to set up, but you first need to have a Google Cloud Platform account and also the Google Cloud command-line tools installed on your local system. Once you have that done, you can simply go into your source code and create an app.yaml file. This file is to configure your Cloud Server. The only thing we need at this point is to specify a runtime of node.js version 12. App Engine will run your code by looking in the package.json file for a start script. So let's go in there and define a start script that runs Node in the current working directory to start our Express app. From there, we can simply open the command line and run gcloud app deploy. That'll take a minute, and then it will give you a URL where you can access your app publicly on the web.
Congratulations, you're now a full-stack cloud architect. If this video helped you, please like and subscribe. And if you really want to dive deep into Node.js and Express, consider becoming a pro member at fireship.io. I have a whole bunch of advanced content covering real-world use cases with these technologies. Thanks for watching, and I will see you in the next one.