In this chapter, we'll learn about Node and Express. We'll understand how Node can help us create performant web services. We'll also understand what the relationship between Node and Express is and how we can use them together to build our web API.
In this chapter, we're going to cover the following main topics:
You should have a basic understanding of web development with JavaScript. We will once again be using Node and Visual Studio Code (VSC).
The GitHub repository is again at https://github.com/PacktPublishing/Full-Stack-React-TypeScript-and-Node. Use the code in the Chap8 folder.
To set up this chapter's code folder, go to your local HandsOnTypescript folder and create a new folder called Chap8.
Node is one of the world's most popular JavaScript frameworks. It is used as the core technology for millions of websites. The reasons for this are many. It is relatively easy to code for. It is also very fast, and when used with things such as clustering and worker threads, it is very scalable. Also, since it uses JavaScript, it allows creating a full-stack application, front to back, using only a single language. All of these characteristics make Node a terrific choice if you are targeting the web. In this section, we will explore Node's architecture and how it achieves its strong performance.
To start, it is important to realize that Node is not a server-specific framework. It is actually a general-use runtime environment, and not just a web server. Node provides JavaScript with capabilities it normally does not have, such as the ability to access the filesystem and accept incoming network connections.
To explain how Node works, let's use the web browser as an analogy. The browser is also a runtime environment for our JavaScript code (as well as HTML and CSS). The browser works by having a core JavaScript engine that provides base-level JavaScript language features. This includes a language interpreter that reads our code for valid JavaScript and a virtual machine that runs our code across differing devices.
Above this core, the browser provides a secure memory container for apps to run on, the sandbox. But it also provides additional JavaScript capabilities generally known as the web API (not the server-side kind but at the browser level). The web API augments the base JavaScript engine, providing things such as Document Object Model (DOM) access so that JavaScript code can access the HTML document and manipulate it. It provides calls such as fetch that allow asynchronous network calls to other machines, as well as WebGL for graphics and many more. You can see a complete list here: https://developer.mozilla.org/en-US/docs/Web/API.
These features are provided as extras, above and beyond what JavaScript comes with "out of the box," which, if you think about it, does make sense since, at its core, JavaScript is just a language and therefore not specific to any particular platform, even the web.
Node follows a similar model to the browser as it also uses a core JavaScript engine (the V8 engine from Google Chrome) and provides a runtime container for our code to run in. However, since it is not a browser, it provides different additional capabilities that are not so focused on a graphical display.
So then, what is Node? Node is a general-use runtime environment that is focused on high performance and scalability. You can build many types of applications with Node, including computer management scripts and terminal programs. But Node's scaling capabilities make it well suited as a web server as well.
Node has many features that make it very capable as a programming runtime, but at its heart is libuv. Libuv is a Node service written in C that interfaces with the operating system kernel and provides asynchronous input/output facilities. In order to make timely access to these services, libuv uses something called an event loop, which we'll explain shortly, to process these tasks. On top of libuv, Node has an add-ons system, which is analogous to Chrome's extensions. It allows developers to extend Node using C++ and add high-performance features that do not exist by default. Also, in order to allow developers to keep using JavaScript to call into C++, a JavaScript-to-C++ binding system is available called Addons. Let's explore libuv and the event loop a bit more.
At the heart of Node are libuv and the event loop. This is the main feature of Node that makes it scale. Libuv's primary job is to provide access to asynchronous input/output (I/O) capabilities of the underlying operating system (Node supports Linux, macOS, and Windows). However, this is not always possible, so it also possesses a thread pool that can be used to make synchronous tasks effectively asynchronous by running them inside a thread. However, the core driver of Node scalability is asynchronous I/O, not threads. The ability to run timers, allow network connections, use operating system sockets, and access the filesystem comes from libuv.
So then, what is the event loop? The event loop is a task runner in libuv, similar to the Chrome event loop, that runs asynchronous callback tasks iteratively. At a high level, here's how it works.
When certain asynchronous tasks are triggered, they get executed by the event loop. The event loop does processing in phases or sets. As shown in the following diagram, it first runs timers, and if any timer callbacks are already queued, it executes them in sequence (if not, it comes back later, and if timers have completed, it queues their callbacks). Then, it handles any pending callbacks (callbacks set by the operating system – for example, TCP errors), and so on, down the line of phases. Note that tasks are asynchronous in nature if they are being executed by libuv, but the callbacks themselves may not be. Therefore, it is possible to block the event loop as it will not trigger the next callback in the queue until the present one returns. Here's a diagram showing roughly how this works:
You can also consider phases as categories of asynchronous tasks and their callbacks.
All frameworks have their strengths and weaknesses. Node's main strength lies in asynchronous I/O-bound scalability. Therefore, Node is best used for highly concurrent workloads that require many simultaneous connections. In later versions of Node, starting at 10.5, the Node team did introduce worker threads to add multithreading capability for running CPU-bound tasks, which are tasks that are mostly about performing long-running computations. However, this is not Node's main strength. For computation-heavy workloads, there are probably better options. But since our priority with Node is to create a highly scalable API for our React frontend, Node works nicely for our needs.
In the next section, we'll start digging deeper into Node by writing code in Node without using any wrapper libraries, such as Express or Koa. This will not only give us a clearer understanding of how the Node core works, but it will also help us to better understand the differences between Node and Express.
In the previous section, we gave a high-level conceptual overview of what Node is and why it scales so well. In this section, we will begin making use of this scalability by writing code with Node. We'll install Node, set up a project, and start exploring the Node API.
Before we can write code with Node, we need to install it. To follow along, in the previous chapters, you may have already done this, but let's refresh our memory of how to install it again as Node gets updated quite frequently:
For production use, you may want to go down the more conservative route and use the Long-Term Support (LTS) version, as shown, but since we want to learn about the latest version for this book, let's select the Current version.
Note
In general, later versions of Node will be slightly faster and have more security and bug fixes. However, it is possible that new issues have been introduced, so you should be careful when upgrading a production server.
By installing Node, we get the runtime as well as the latest npm package manager.
Complete the installation as guided by the setup window.
Great, now we have installed or updated our Node runtime and npm package manager. As mentioned previously, Node is not just a server framework but also a complete runtime environment that allows you to write a great variety of different applications. For example, Node has a command-line interface called REPL. If you open your command line or terminal and type node, you will see that it converts to accept JavaScript commands, like this:
We will not be using REPL in this book, but I included it here so that you are aware that it exists and may be of use for your future projects. You can learn more about REPL in the official documentation, https://nodejs.org/api/repl.html#repl_design_and_features. Also, in case you're curious, undefined is returned because each command has not returned anything and in JavaScript, that is always undefined.
Okay, so now, let's create our first Node application and explore some more of Node's features:
console.log("hello world");
Then, run it:
node app.js
You should see the following output:
Not a particularly useful application, but nevertheless, as you can see, Node is running standard JavaScript code. Now, let's do something more useful. Let's access the filesystem using the following steps:
const fs = require("fs");
You may be confused by this code since it's not the current style of doing imports. But I wanted to include it here since a great deal of older Node code still uses this CommonJS-style syntax for importing dependencies. So, you should be aware of it.
fs.writeFile("test.txt", "Hello World", () => {
fs.readFile("test.txt", "utf8", (err, msg) => {
console.log(msg);
});
});
If you run this code, you will see the following output and a file called test.txt created in your try-node folder:
The syntax of fs is a bit cumbersome because it uses the older callback style. Node was created long before JavaScript received Promises and async await, and therefore some calls still use the callback style of asynchrony. However, there is a newer, Promise-enabled version of fs – which is, therefore, async await-capable – that can be used instead. Its syntax looks like this:
const fs = require("fs/promises");
(async function () {
await fs.writeFile("test-promise.txt", "Hello Promises");
const readTxt = await fs.readFile("test-promise.txt", "utf-8");
console.log(readTxt);
})();
Notice we are using an IIFE to allow us to make a top-level await call.
If you are on an older version of Node, fs/Promises became stable after version 11, so you can use a tool called promisify to wrap callback-style calls to get them to work in an async await style.
Nevertheless, it is important that you are aware of the older callback-style calls since this is historically how Node code was written and there is probably a great deal of Node code that remains in this style today.
import fs from "fs";
If you run app.mjs again, you will see that it still works. We could set the configuration flag inside of package.json "type" : "module" instead, but for this example app, we did not use npm. Additionally, if we set this flag globally, we can no longer use require. This can be an issue as some older npm dependencies still use require for importing.
Note
There is an older command-line flag called --experimental-modules that allows the use of import, but it is now deprecated and should be avoided for newer versions of Node.
We learned that Node is based on some older JavaScript technologies, such as callbacks and CommonJS. Node was created before JavaScript Promises and the newer versions of JavaScript, such as ES6 and beyond. Nevertheless, Node still works well, continues to be updated, and later, when we add additional libraries, we'll be able to use async await and Promises in most cases.
Now, let's work on a more realistic Node server example. We'll create a new project with npm:
npm init
import http from "http";
Don't worry, we'll start using TypeScript soon. Right now, let's keep things simple so that we can focus on learning Node.
At the end of our createServer handler function, we are explicitly ending our calls by using res.end and returning text. If we did not send end, our response would never complete and nothing would appear on the browser:
const server = http.createServer((req, res) => {
console.log(req);
res.end("hello world");
});
const port = 8000;
server.listen(port, () => {
console.log(`Server started on port ${port}`);
});
node server.mjs
Please remember, as we work, that there is no auto-reload function currently. So, upon code changes, we will have to do a stop and restart manually. We'll add this later as we continue adding more features to our projects.
The terminal is showing the req object and its members. We'll of course go over Request and Response in more detail soon.
Another interesting thing is that no matter what URL we give, it always returns the same hello world text. This is because we have not implemented any route handling. Handling routes is another item we must learn in order to use Node properly.
You can keep refreshing your browser and the server will keep responding with hello world. As you can see, the server remains up no matter how many requests we send it, as opposed to returning and ending like a typical script program. This is because the event loop, the core of Node, is a kind of infinite loop that will keep waiting for new tasks and processing them dutifully.
Congratulations, you've now run your first Node server! Humble beginnings for sure, but nevertheless, you can now make real browser calls and our server will respond. So, you are well on your way.
When a request from a browser gets to the server, all server frameworks will generally have two objects: Request and Response. These two objects represent the relevant data for the request that came from the browser and the response that will be returned to it. Let's take a look at these objects from the browser to see what they're made of. Reload your browser but this time with the Chrome dev tools open on the Network tab:
This view is only from the browser's perspective and in Node, there's a lot more information in these objects. However, we need to first understand what a web request is made of before we can try and create any real web server. So, let's list out some of the more important items and describe what they mean.
Obviously, this represents the complete URL path sent to the server. But the reason the server needs to know the complete path is that a lot of additional information can often be sent in the URL. For example, if our URL was http://localhost:8000/home?userid=1, there's actually a fair amount of information here. First, we tell our server that we're looking for either a web page or API data within the home subdirectory. This allows the server to tailor its returned response with only an HTML page or data specific to that URL. Additionally, we've passed a parameter called userid (parameters start after the question mark and multiple parameters can be separated by the & symbol), which can be used by the server to provide unique data in the request.
A request method represents what's called an HTTP verb. A verb is simply a description that tells the server what action the client is intending to do. The default verb is GET, which means, as the name indicates, the browser would like to read some data. The other verbs are POST, which indicates creation or insertion, PUT, which indicates updating, and then DELETE, indicating deletion. In Chapter 9, What Is GraphQL?, we'll see that GraphQL only uses the POST method, but this isn't actually an error as the verbs are not hard rules but more like guidelines. One additional thing to note is that when using GET, any parameters needed will be provided in the URL, as the request URL bullet item example showed. However, for POST, the parameters are provided in the body of the request. We'll go over these differences more in the Learning Express capabilities section.
All web requests will return these codes to indicate the result of the request. For example, a status of 200 indicates success. I won't list all of them here, but we should know some of the most common ones, as it can sometimes help in debugging:
Headers provide additional information that acts as descriptions or metadata. As shown, there are multiple types of headers: general, request, response, and entity. Again, I won't go over all of them, but there are a few we should be familiar with. Here are the request headers:
And here are the response headers:
This is, of course, dry information. However, knowing what's involved in making these requests and responses helps us better understand how the web works and therefore write better web apps. Let's look more deeply now at routing.
Routing is in some sense a lot like passing parameters to a server. When the server sees a specific route, it will know that the response needs to be a certain way. The response could be returning some specific data or writing data to a database, but having routes helps us manage how our server should behave for each request.
Let's do some route handling in Node:
const server = http.createServer((req, res) => {
if (req.url === "/") {
res.end("hello world");
} else if (req.url === "/a") {
res.end("welcome to route a");
} else if (req.url === "/b") {
res.end("welcome to route b");
} else {
res.end("good bye");
}
});
As you can see, we take the req.url field and compare it to several URLs. For each one that matches, we end our response with some unique text.
const server = http.createServer((req, res) => {
if (req.url === "/") {
res.end("hello world");
} else if (req.url === "/a") {
res.end("welcome to route a");
} else if (req.url === "/b") {
res.end("welcome to route b");
} else if (req.url === "/c" && req.method === "POST") {
let body = [];
req.on("data", (chunk) => {
body.push(chunk);
});
req.on("end", () => {
const params = Buffer.concat(body);
console.log("body", params.toString());
res.end(`You submitted these parameters:
${params.toString()}`);
});
} else {
res.end("good bye");
}
});
As you can see, we added another if else statement with the /c route and the POST method type. You may be surprised to see that in order to get the posted data from our call, we need to handle the data event and then the end event so that we can return the call.
Let me explain what this is about. Node is very low level, meaning it does not hide its intricate details to make things easier, in order to be more performant. So, when a request is made and some information is being sent to the server, this data will be sent as a stream. This simply means the data is not sent in one shot, but in pieces. Node does not hide this fact from the developer and uses an event system to receive the data in chunks, since it's not clear upfront how much data is coming in. Then, once this data is finished being received, that's when the end event triggers.
In this sample, the data event is used to aggregate our data into an array. Then, the end event is used to put that array into a memory buffer, from which it can then be processed as a whole. In our case, it's just JSON, so we convert to a string.
curl --header "Content-Type: application/json" --request POST --data '{"userid":"1","message":"hello"}' "http://localhost:8000/c"
You should get the following back:
Clearly, all of this works but is not ideal from a development productivity perspective. We would not want 30 of these if else statements in a single createServer function. It's difficult to read and maintain. We'll see how Express helps us avoid these types of issues by providing extra wrappers on top of Node to make development faster and more reliable. We'll see this in the Understanding how Express improves Node development section. Let's learn about some tools to help our Node coding first.
As we saw with React, a debugger is a very important tool to help in troubleshooting code. In the case of Node, we cannot use a browser tool, of course, but VSCode does have a built-in debugger that will allow us to break on code and view values. Let's take a look at that as we'll also use it with Express:
The first button runs the debugger and the second shows the debugger version of the terminal. When running the debugger, you generally want to be looking at the debugger console as it can show errors that happen at runtime.
As you can see, we have set a breakpoint on line 13 within the data event and are able to view the current chunk. Hit the continue button or click F5 to continue running the program.
Look at our breakpoint, toward the middle of the screenshot. We can see that we have broken inside of the end event handler scope. Let's look at some of the features listed:
That was a quick introduction to the VSCode debugger. We will be using more of it as we get into Express, and then later with GraphQL.
Now, as you've seen, having to manually restart the Node service every time we make any changes is a bit of a pain and slows down development. So, let's use a tool called nodemon, which will automatically restart our Node server whenever we save script changes:
npm i nodemon -g
This installs nodemon to our entire system. Installing it globally allows all apps to run nodemon without needing to keep installing it. Note that on macOS and Linux, you may need to prefix this command with sudo, which will elevate your rights so that you can install it globally.
nodemon server.mjs
Your package.json "scripts" section should look like this now:
npm start
Note that normally, when running the npm command, you need to run npm run <file name>. However, for start scripts, we can skip the run sub-command.
You should see the app start up as usual.
If you click this button, you will then see the launch.json file in the .vscode folder in the root of the GitHub repository (not the root of the project). It should contain the following configurations (note that this one file can contain multiple configurations):
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit:
// https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch node-server Program",
"skipFiles": ["<node_internals>/**"],
"program": "${workspaceFolder}/Chap8/node-
server/server.mjs",
"runtimeExecutable": "nodemon",
"restart": true,
"console": "integratedTerminal"
}
]
}
You can see that the configurations field is an array, which means you can keep adding configurations to this one file. But for our configuration, notice that type is node, of course. We've also updated name to "Launch node-server Program". But also, notice that we switched runtimeExecutable to be nodemon instead of node, and console is now the integrated terminal. In order to use nodemon with the debugger, we must switch to the TERMINAL tab and not the debugger console.
If your dropdown does not show Launch node-server Program, select it and then press the play button. Then, you should see the debugger launch again, only this time it will auto-restart.
That was a fast introduction to some tools that will help with our development and debugging.
In this section, we learned about using Node directly to code our server. We also learned about debugging and tooling to improve our development flow. Coding with Node directly can be time-consuming and unintuitive. In the next sections, we will learn about Express and how it helps make our Node development experience better.
As we have seen, coding with Node directly has an awkward and cumbersome sort of feel. Having an easier-to-use API would make us more productive. This is what the Express framework attempts to do. In this section, we will learn what Express is and how it can help us write code more easily for our Node apps.
Express is not a standalone JavaScript server framework. It is a layer of code that sits on top of Node, and therefore uses Node, to make developing JavaScript servers with Node both easier and more capable. Just like Node, has its own core capabilities and then some additional features via dependency packages. Express also has its core abilities as well as a rich ecosystem of middleware that provides extra capabilities.
So then, what is Express? According to the website, Express is just an application that is a series of middleware calls. Let's explain this by first looking at a diagram:
Whenever a new server request comes in, it travels along a sequential path for processing. Normally, you would just have a request, and then once the request is understood and processed, you would then get some response. When using Express, however, you can have multiple intermediate functions that inject themselves into the process and do some unique work.
So, in the example shown in Figure 8.22, we see that first there is middleware that adds CORS ability, which is a way to allow requests from different URL domains than the one that the server resides on. Then, we have middleware that handles sessions and cookies. Sessions are just unique data that gets collected about the user's current usage of the website – for example, their login ID. Then, finally, we see an on-error handler that will determine some unique message that will be shown depending on the error that has occurred. Of course, you could add more middleware as needed. The key point here is that Express enables this injection of extra capabilities that Node normally does not have in a fairly straightforward way.
On top of this middleware ability, Express adds additional features to the Request and Response objects that further enhance the developer's productivity. We'll take a look at those features and further explore Express in the next section.
Express is basically a middleware runner for Node. But, like most things in life, a simple explanation seldom provides the information necessary to make proper use of it. So, in this section, we will explore Express and learn about its capabilities with examples.
Let's install Express onto our node-server project. Type the following command into your terminal:
npm I express -S
This should give you an updated package.json file that has a new dependencies section:
Now, before we start writing code, we need to understand a few things. Again, as mentioned, Express is a wrapper around Node. This means that Express already uses Node internally. So, when we write code with Express, we will not be directly calling Node. Let's see what this looks like:
import express from "express";
const app = express();
app.listen({ port: 8000 }, () => {
console.log("Express Node server has loaded!");
});
As you can see, we create an instance of express and then we call a function called listen on it. Internally, the express.listen function calls Node's createServer and listen functions. If you run this file, you will see the following log message:
So, now we have a running Express server. However, it does not do anything until we add some middleware. Middleware for Express runs under several main umbrellas or sections. There is middleware that runs for the entire application, there is middleware that runs only during routing, and there is middleware that runs on errors. There is also middleware that is core to Express that it uses internally. And, of course, instead of implementing our own code to do the work of middleware, we can also use npm packages that provide third-party middleware. We already saw some of these in the previous section, Understanding how Express improves Node development, in Figure 8.22.
import express from "express";
const app = express();
app.use((req, res, next) => {
console.log("First middleware.");
next();
});
app.use((req, res, next) => {
res.send("Hello world. I am custom middleware.");
});
app.listen({ port: 8000 }, () => {
console.log("Express Node server has loaded!");
});
So, for this first example, we have decided to use the app-level middleware by using the use function on the app object. This means that any requests for the entire application, regardless of route, will have to process these two middleware.
Let's go through them. First, note that all middleware is processed in the order that they are declared in code. Second, unless ending a call at the end of a middleware, we must call the next function to go to the next middleware, or else the processing will stop.
The first middleware is just logging some text, but the second middleware will write to the browser screen by using the Express send function. The send function is a lot like the end function in Node, as it ends processing, but it also sends back a content-type header of the text/html type. If we were using Node, we would have to send headers explicitly ourselves.
import express from "express";
const router = express.Router();
First, we have created our new router object from the express.Router type:
const app = express();
app.use((req, res, next) => {
console.log("First middleware.");
next();
});
app.use((req, res, next) => {
res.send("Hello world. I am custom middleware.");
});
app.use(router);
So, we have the same set of middleware as before added to the app object, which makes it run globally across all routes. But then, we have also added the router object to our app as middleware. The router middleware, however, runs only for the specific routes that are defined:
router.get("/a", (req, res, next) => {
res.send("Hello this is route a");
});
router.post("/c", (req, res, next) => {
res.send("Hello this is route c");
});
So again, we added two middleware to our router object: one for the /a route, which uses the get method function and the other for the /c route, which uses the post method function. Again, these functions represent the HTTP verbs that are possible. The listen function call is identical to before:
app.listen({ port: 8000 }, () => {
console.log("Express Node server has loaded!");
});
Now, a peculiar thing will happen if we run this code by going to the following URL: http://localhost:8000/a. Instead of seeing the Hello this is route a text, we will instead see the Hello world. I am custom middleware. text. Can you guess why? It is because middleware order matters and since our second app-level middleware is calling res.send, all calls will end there and not go to the next middleware.
Remove the second app.use call that sends the Hello world… message and try going to http://localhost:8000/a. You should see the following message now:
Great, that worked, but now try using your browser to go to http://localhost:8000/c. Does that work? No, it does not, and you get the Cannot GET /c message. As you might have guessed, browser calls are defaulted to GET calls but our /c route is a POST-only route. If you bring up your terminal and run the last POST curl command that we used in the Learning Node's capabilities section, you'll see this:
As you can see, we receive the appropriate text message.
import express from "express";
import bodyParser from "body-parser";
We first import the body parser middleware. Then, we add the body parser at the top so that all of our handlers can be automatically converted from a JSON string to a parsed object when needed:
const router = express.Router();
const app = express();
app.use(bodyParser.json());
Then, we update our /c route handler so that its text message shows the value passed in the message field:
app.use((req, res, next) => {
console.log("First middleware.");
next();
});
app.use(router);
router.get("/a", (req, res, next) => {
res.send("Hello this is route a");
});
router.post("/c", (req, res, next) => {
res.send(`Hello this is route c. Message is
${req.body.message}`);
});
As you can see, most of this work is quite a bit easier than using Node events such as data and end.
import express from "express";
import bodyParser from "body-parser";
const router = express.Router();
const app = express();
app.use(bodyParser.json());
app.use((req, res, next) => {
console.log("First middleware.");
throw new Error("A failure occurred!");
});
Then, we throw an error from our first custom middleware:
app.use(router);
router.get("/a", (req, res, next) => {
res.send("Hello this is route a");
});
router.post("/c", (req, res, next) => {
res.send(`Hello this is route c. Message is ${req.body. message}`);
});
app.use((err, req, res, next) => {
res.status(500).send(err.message);
});
Now, we've added our error handler as the last middleware in our code. This middleware will catch all errors that were not previously handled and send the same status and message:
app.listen({ port: 8000 }, () => {
console.log("Express Node server has loaded!");
});
Since our top-level middleware is throwing an exception, all routes will throw this exception and therefore will be caught by our error handler middleware.
This was an overview of the Express framework and its features. As you can see, it can make developing with Node quite a bit simpler and clearer. In the next section, we will take a look at using Express and Node to build a web API that returns JSON, which is the default data schema of the web.
In this section, we will learn about the web API. Currently, it is one of the most popular ways of providing data across the web. In our final application, we will not be using the web API since it is our intention to use GraphQL. However, it is good to have some understanding of web API design since on the internet, it is very commonly used and under the covers, GraphQL also works similarly.
What is a web API? API stands for application programming interface. This means that it's a way for one programming system to interact with another. Therefore, a web API is an API that uses web technologies to provide programming services to other systems. A web API sends and receives data as a string, as opposed to binary data, and usually in JSON format.
All web APIs will have an endpoint that is represented by a URI, basically the same thing as a URL. This path must be static and not changing. If a change is desired, then it is expected that the API vendor will do a version update, leaving the old URI intact and creating a new URI delineated by a version upgrade. For example, if a URI started as /api/v1/users, then the next iteration would be /api/v2/users.
Let's create a simple web API for demonstration purposes:
import express from "express";
import bodyParser from "body-parser";
const router = express.Router();
const app = express();
app.use(bodyParser.json());
app.use((req, res, next) => {
console.log("First middleware.");
next();
});
Everything up to here was the same, except notice we removed the exception that was thrown:
app.use(router);
router.get("/api/v1/users", (req, res, next) => {
const users = [
{
id: 1,
username: "tom",
},
{
id: 2,
username: "jon",
},
{
id: 3,
username: "linda",
},
];
console.log(req.query.userid);
const user = users.find((usr) => usr.id == req.query. userid);
res.send(`User ${user?.username}`);
});
This first middleware is showing the /api/v1/users path. This type of pathing is fairly standard for web APIs. It indicates the version and a related container of data to query – in this case, users. For example purposes, we are using a hardcoded array of users and finding only one with a matching ID. Since id is a number and anything coming from req.query is a string, we are using == as opposed to ===. If you load the browser to the URI, you should see this:
As you can see, our second user, jon, is returned.
router.post("/api/v1/groups", (req, res, next) => {
const groups = [
{
id: 1,
groupname: "Admins",
},
{
id: 2,
groupname: "Users",
},
{
id: 3,
groupname: "Employees",
},
];
const group = groups.find((grp) => grp.id == req.body. groupid);
res.send(`Group ${group.groupname}`);
});
If you run the terminal command to this URI, you should see the following:
As shown, our first group, Admins, is returned. The rest of the code is identical:
app.use((err, req, res, next) => {
res.status(500).send(err.message);
});
app.listen({ port: 8000 }, () => {
console.log("Express Node server has loaded!");
});
Important Note
Since the web API is specific to web technologies, it supports calls with all the HTTP methods: GET, POST, PATCH, PUT, and DELETE.
This was a quick introduction to building a web API using Express and Node. We now have had a broad overview of Node and its most important framework, Express.
In this chapter, we learned about Node and Express. Node is the core server-side technology that drives a huge percentage of servers on the web, and Express is the most popular and often used Node-based framework for building web applications. We now have a complete picture of how frontend and backend technologies work to create websites.
In the next chapter, we'll learn about GraphQL, an extremely popular and relatively new standard for creating web-based API services. Once we've covered this, we will have all the knowledge we need to start building our project.
3.18.220.243