Take a look at what we created in the previous lesson:
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:
1mkdir <work_dir>
Replace <work_dir>
with your desired directory name.
Change into the work directory:
1cd <work_dir>
Initialize a new Node.js project:
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"
:
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:
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.
1touch index.js
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:
1node index.js
The following output will be printed to the terminal.
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.
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.
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.
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.
After installing the extension, a new Thunder Client tab should show up.
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:
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 (:
).
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.
It is also possible to define multiple parameters for one route.
1app.get("/users/:id/posts/:slug", (req, res) => {
2 res.send(req.params);
3});
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.
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:
1First route handler executed.
2Second route handler executed.
3Third route handler executed.
And a response will be returned to the client.
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.
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.
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});
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:
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.
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
.
1.
2├── index.js
3├── package-lock.json
4├── package.json
5└── routes
6 └── dashboard.js
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.
1import dashboardRouter from "./routes/dashboard.js";
2
3app.use("/dashboard", dashboardRouter);