A Beginner's Guide to Express.js

Take a look at what we created in the previous lesson:

javascript
1import { createServer } from "http";
2import { writeFile, readFile } from "fs";
3
4let server = createServer((request, response) => {
5  if (request.method === "GET") {
6    . . .
7  } else if (request.method === "POST") {
8    . . .
9  }
10});
11
12server.listen(8000);
13
14console.log("Listening! (port 8000)");

For a single route application (/), we had to consider two different request methods, GET and POST.

Imagine the application has several other routes, and for each route, we have to deal with at least two request methods. This app will become very difficult to maintain. Not to mention we haven't discussed the security measures, testing and logging practices, how to split the frontend and backend code, and so on.

This is why frameworks such as Express.js are created to speed up our development cycle.

It doesn't matter what kind of application you are building, there are always some tools and components that you are going to need, such as routing, database integration, template system, and so on. This is what Express is for, it is a set of tools and components that you are going to need when building web applications.

Installing Express.js

To initialize a new Express application, create a new work directory. Of course, you could do things through the graphical interface, but after investing so much time and resources trying to become a programmer, let's do things the programmer way.

Open the terminal and run the following command:

bash
1mkdir <work_dir>

Replace <work_dir> with your desired directory name.

Change into the work directory:

bash
1cd <work_dir>

Initialize a new Node.js project:

bash
1npm init

This command will create a new package.json file and prompt you with a few questions. It is safe to keep everything to default, as you can always change it later.

Open the newly created package.json file and add a new item, "type": "module":

json
1{
2  "name": "expressjs",
3  "type": "module",
4  "version": "1.0.0",
5  "description": "",
6  "main": "index.js",
7  "scripts": {
8    "test": "echo \"Error: no test specified\" && exit 1"
9  },
10  "author": "",
11  "license": "ISC",
12  "dependencies": {
13    "express": "^4.19.2",
14    "sqlite3": "^5.1.7"
15  }
16}

As we've explained in the JavaScript Modules lesson, this allows us to import packages as ES modules, which is the modern way to deal with packages.

Finally, use npm to install the express package:

bash
1npm install express

Hello, World!

Now, let's use the Express.js package to build a web application. Create an index.js file, which will be the entry point of your app.

bash
1touch index.js
javascript
1import express from "express";
2
3const app = express();
4const port = 3001;
5
6app.get("/", (req, res) => {
7  res.send("Hello, World!");
8});
9
10app.listen(port, () => {
11  console.log(`App listening on port ${port}. Visit http://localhost:${port}.`);
12});

The express() is a function exported by the express package. It creates a new Express application and gives us access to everything included in the framework.

The get() method allows you to work with the GET method. The method takes two arguments: a route, which is "/" in this case, and a callback function, which allows you to define exactly what to do when the server receives a GET request for the route /.

The callback function allows for two input arguments. The first one gives you access to the HTTP request, and the second one allows you to define the HTTP response that should be returned. In this example, we are using the send() method to send the string "Hello, World!".

Lastly, the server needs to be listening on a port, which is 3001 in this case. The listen() method can also take a callback function, which will be executed when the server starts. It is often used to inform you which port the app is hosted on, as well as how to access it.

You can now start the server with the following command:

bash
1node index.js

The following output will be printed to the terminal.

text
1App listening on port 3001. Visit http://localhost:3001.

Open your browser and go to http://localhost:3001/. This will send a GET request to our app, and as we just discussed, a string "Hello, World!" should be returned.

Response with Hello, World!

Basic routing

For most web applications, you need to allow the user to navigate to different pages through different URL endpoints.

In our demo app, we defined the root route (/), which usually points to the homepage of your website.

javascript
1app.get("/", (req, res) => {
2  res.send("Hello, World!");
3});

You may define other routes in a similar way. For instance, if you are building a blog application, there could be a /posts route that displays a list of all posts, and a /users route that shows all registered users for the blog.

javascript
1app.get("/users", (req, res) => {
2  res.send(`
3  <ul>
4      <li><strong>Username:</strong> JohnDoe | <strong>Email:</strong> johndoe@example.com</li>
5      <li><strong>Username:</strong> JaneSmith | <strong>Email:</strong> janesmith@example.com</li>
6      <li><strong>Username:</strong> BobJohnson | <strong>Email:</strong> bobjohnson@example.com</li>
7  </ul>`);
8});

The callback function is sometimes referred to as a route handler. You often need to perform certain tasks inside the route handler, such as retrieving information from the database, before sending a response using res.send().

Route methods

Besides get(), Express also supports other HTTP methods, such as post(), put(), delete(), and head().

These function methods are named exactly like their HTTP method counterparts. For example, post() is often used to add new resources to the server, and delete() is used to remove resources from the server.

We will discuss exactly how to perform these actions later. For now, we are only focused on how to define routes.

Thunder Client

Sometimes, it is difficult to test the routes using the browser, especially for POST and PUT requests, where you need to send extra data to the server. We'll have to create the corresponding web form to fill out the data, which can be very time-consuming.

To make things easier, people have created many tools that allow you to send HTTP requests to any URL endpoints, and also get a response without having to create a user interface. For this course, we'll use Thunder Client as an example, which is available as a VSCode extension.

Thunder Client

After installing the extension, a new Thunder Client tab should show up.

Thunder Client UI

Thunder Client has a fairly intuitive user interface. To create a new request, follow the steps illustrated in the above graph.

  • Step 1: Click the New Request button.
  • Step 2: Choose the request method.
  • Step 3: Type in the URL endpoint.
  • Step 4: Inside the request body, you can provide the data that you need to pass to the server. The data can be encoded in different formats, such as JSON, XML, Form, and so on. They may be treated differently on the server side. We'll discuss more about their differences later.
  • Step 5: Click Send.

Passing parameters

Imagine you have a route that looks like /users/johndoe, which displays a profile page for the user johndoe. Of course, you could hardcode the route like this:

javascript
1app.get("/users/johndoe", (req, res) => {
2  res.send(. . .);
3});

However, as you can imagine, this solution is very impractical because you will have to create a new route every time a new user registers.

A better way to solve this problem is to define the second part of the URL as a parameter, which is marked with a colon (:).

javascript
1app.get("/users/:id", (req, res) => {
2  res.send(req.params);
3});

The parameters will be automatically parsed from the browser, and then transferred to the backend along with the HTTP request, which allows them to be accessed through req.params. In practice, you can then use this parameter to retrieve the corresponding user data from the database, and then deliver it to the user.

Getting route parameters

It is also possible to define multiple parameters for one route.

javascript
1app.get("/users/:id/posts/:slug", (req, res) => {
2  res.send(req.params);
3});

Multiple route parameters

Route handlers and the middleware

Next, let's take a closer look at the second argument accepted by get(), the callback function, which is also referred to as the route handler. You can create multiple route handlers for each route.

javascript
1app.get(
2  "/",
3  (req, res, next) => {
4    console.log("First route handler executed.");
5    next();
6  },
7  (req, res, next) => {
8    console.log("Second route handler executed.");
9    next();
10  },
11  (req, res) => {
12    console.log("Third route handler executed.");
13    res.send("Hello, World!");
14  }
15);

The callback takes a third parameter, next, which is tied to a function. When executed, this will pass the control to the next route handler. Without it, JavaScript won't know where to go after the action defined in the route handler is completed, and the user request will be left hanging without returning a response.

Also, the response (res.send()) should be returned in the last route handler, because it also marks the end of the entire handler chain. Any route handlers defined afterward will be ignored.

For our example, when a GET request is sent to the root URL (/), the following messages will be printed to the console:

text
1First route handler executed.
2Second route handler executed.
3Third route handler executed.

And a response will be returned to the client.

text
1Hello, World!

So why do we need multiple handlers at all? You can easily rewrite our previous example into a single route handler, and it will give the same result.

javascript
1app.get("/", (req, res) => {
2  console.log("First route handler executed.");
3  console.log("Second route handler executed.");
4  console.log("Third route handler executed.");
5  res.send("Hello, World!");
6});

And also, why do we need a next() function? Why can't Express just automatically move on to the next handler function?

This is related to another new concept called the middleware. It is a piece of code that will execute after a request has been received, but before a response is returned. It is a program that exists in the middle, hence the name middleware.

The reason we need multiple route handlers is to create middleware functions that only perform one specific task. You can then reuse this middleware somewhere else.

For example, you could create a middleware that checks if the user is logged in, and only the authenticated user can access certain route.

javascript
1function auth(req, res, next) {
2  // Check if the user is authenticated
3  let isAuthenticated = false;
4
5  if (isAuthenticated) {
6    next();
7  } else {
8    res.status(401).send("User not authenticated.");
9  }
10}
11
12app.get("/users", auth, (req, res) => {
13  res.send(. . .);
14});

Unauthorized

This exact same auth() middleware can be reused for other routes.

Our example also answers the second question, why do we need next()? It allows you to create a flow control system. Notice that in our example, because user is not authenticated, instead of moving on to the next route handler, the 401 response is returned.

The route() method

The route() method allows you to organize the routes in a different and more logical way. For example, our /users/:id route accepts GET, POST, and DELETE requests:

javascript
1app.get("/users/:id", (req, res) => {
2  res.send(`Show user profile for ${req.params.id}`);
3});
4
5app.post("/users/:id", (req, res) => {
6  res.send(`Create new user ${req.params.id}`);
7});
8
9app.delete("/users/:id", (req, res) => {
10  res.send(`Delete user ${req.params.id}`);
11});

In this structure, we are forced to define the HTTP method first, and then provide the matching URL paths. Using the route() method, you can define the URL endpoint first, and then specify the HTTP methods that the endpoint can work with.

javascript
1app
2  .route("/users/:id")
3  .get((req, res) => {
4    res.send(`Show user profile for ${req.params.id}`);
5  })
6  .post((req, res) => {
7    res.send(`Create new user ${req.params.id}`);
8  })
9  .delete((req, res) => {
10    res.send(`Delete user ${req.params.id}`);
11  });

Nested routes

As your application gets more and more complex, you might want to consider modulating the routes instead of putting everything in one file. For example, create a new directory, routes, and a new file dashboard.js inside the routes directory. This file will be used to store routes under /dashboard.

text
1.
2├── index.js
3├── package-lock.json
4├── package.json
5└── routes
6    └── dashboard.js
javascript
1import { Router } from "express";
2const router = Router();
3
4router.get("/user/:id", (req, res) => {
5  res.send("Show user");
6});
7
8router.delete("/user/:id", (req, res) => {
9  res.send("Delete user");
10});
11
12router.post("/user/new", (req, res) => {
13  res.send("Create user");
14});
15
16export default router;

And then, go back to the index.js file, and import the /dashboard routes.

javascript
1import dashboardRouter from "./routes/dashboard.js";
2
3app.use("/dashboard", dashboardRouter);