Skip to content

Update Express tutorial to v5 #40247

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ The server computer could be located on your premises and connected to the Inter
This sort of remotely accessible computing/networking hardware is referred to as _Infrastructure as a Service (IaaS)_. Many IaaS vendors provide options to preinstall a particular operating system, onto which you must install the other components of your production environment. Other vendors allow you to select more fully-featured environments, perhaps including a complete Node setup.

> [!NOTE]
> Pre-built environments can make setting up your website easier because they reduce the configuration, but the available options may limit you to an unfamiliar server (or other components) and may be based on an older version of the OS. Often it is better to install components yourself so that you get the ones that you want, and when you need to upgrade parts of the system, you have some idea of where to start!
> Pre-built environments can make setting up your website easier because they reduce the required configuration, but the available options may limit you to an unfamiliar server (or other components) and may be based on an older version of the OS. Often it is better to install components yourself so that you get the ones that you want, and when you need to upgrade parts of the system, you have some idea of where to start!

Other hosting providers support Express as part of a _Platform as a Service_ (_PaaS_) offering. When using this sort of hosting you don't need to worry about most of your production environment (servers, load balancers, etc.) as the host platform takes care of those for you. That makes deployment quite straightforward because you just need to concentrate on your web application and not any other server infrastructure.

Expand Down Expand Up @@ -107,16 +107,16 @@ In the following subsections, we outline the most important changes that you sho

### Database configuration

So far in this tutorial, we've used a single development database, for which the address and credentials are hard-coded into **app.js**.
So far in this tutorial, we've used a single development database, for which the address and credentials were [hard-coded into **bin/www**](/en-US/docs/Learn_web_development/Extensions/Server-side/Express_Nodejs/mongoose#connect_to_mongodb).
Since the development database doesn't contain any information that we mind being exposed or corrupted, there is no particular risk in leaking these details.
However if you're working with real data, in particular personal user information, then protecting your database credentials is very important.
However if you're working with real data, in particular personal user information, then it is very important to protect your database credentials.

For this reason we want to use a different database for production than we use for development, and also keep the production database credentials separate from the source code so that they can be properly protected.

If your hosting provider supports setting environment variables through a web interface (as many do), one way to do this is to have the server get the database URL from an environment variable.
Below we modify the LocalLibrary website to get the database URI from an OS environment variable, if it has been defined, and otherwise use the development database URL.

Open **app.js** and find the line that sets the MongoDB connection variable.
Open **bin.www** and find the line that sets the MongoDB connection variable.
It will look something like this:

```js
Expand All @@ -127,19 +127,9 @@ const mongoDB =
Replace the line with the following code that uses `process.env.MONGODB_URI` to get the connection string from an environment variable named `MONGODB_URI` if has been set (use your own database URL instead of the placeholder below).

```js
// Set up mongoose connection
const mongoose = require("mongoose");

mongoose.set("strictQuery", false);

const dev_db_url =
"mongodb+srv://your_user_name:your_password@cluster0.cojoign.mongodb.net/local_library?retryWrites=true&w=majority";
const mongoDB = process.env.MONGODB_URI || dev_db_url;

main().catch((err) => console.log(err));
async function main() {
await mongoose.connect(mongoDB);
}
```

> [!NOTE]
Expand All @@ -166,7 +156,7 @@ The debug variable is declared with the name 'author', and the prefix "author" w
const debug = require("debug")("author");

// Display Author update form on GET.
exports.author_update_get = asyncHandler(async (req, res, next) => {
exports.author_update_get = async (req, res, next) => {
const author = await Author.findById(req.params.id).exec();
if (author === null) {
// No results.
Expand All @@ -177,7 +167,7 @@ exports.author_update_get = asyncHandler(async (req, res, next) => {
}

res.render("author_form", { title: "Update Author", author });
});
};
```

You can then enable a particular set of logs by specifying them as a comma-separated list in the `DEBUG` environment variable.
Expand Down Expand Up @@ -256,7 +246,7 @@ const app = express();
app.use(
helmet.contentSecurityPolicy({
directives: {
"script-src": ["'self'", "code.jquery.com", "cdn.jsdelivr.net"],
"script-src": ["'self'", "cdn.jsdelivr.net"],
},
}),
);
Expand All @@ -265,7 +255,7 @@ app.use(
```

We normally might have just inserted `app.use(helmet());` to add the _subset_ of the security-related headers that make sense for most sites.
However in the [LocalLibrary base template](/en-US/docs/Learn_web_development/Extensions/Server-side/Express_Nodejs/Displaying_data/LocalLibrary_base_template) we include some bootstrap and jQuery scripts.
However in the [LocalLibrary base template](/en-US/docs/Learn_web_development/Extensions/Server-side/Express_Nodejs/Displaying_data/LocalLibrary_base_template) we include some bootstrap scripts.
These violate the helmet's _default_ [Content Security Policy (CSP)](/en-US/docs/Web/HTTP/Guides/CSP), which does not allow loading of cross-site scripts.
To allow these scripts to be loaded we modify the helmet configuration so that it sets CSP directives to allow script loading from the indicated domains.
For your own server you can add/disable specific headers as needed by following the [instructions for using helmet here](https://www.npmjs.com/package/helmet).
Expand Down Expand Up @@ -325,7 +315,7 @@ Open **package.json**, and add this information as an **engines > node** as show

```json
"engines": {
"node": ">=16.17.1"
"node": ">=22.0.0"
},
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,6 @@ sidebar: learnsidebar

Now that you know what [Express](/en-US/docs/Learn_web_development/Extensions/Server-side/Express_Nodejs/Introduction#introducing_express) is for, we'll show you how to set up and test a Node/Express development environment on Windows, or Linux (Ubuntu), or macOS. For any of those operating systems, this article provides what you need to start developing Express apps.

> [!WARNING]
> The Express tutorial is written for Express version 4, while the latest version is Express 5.
> We plan to update the documentation to support Express 5 in the second half of 2025. Until then, we have updated the installation commands so they install Express 4 rather than the latest version, to avoid any potential compatibility problems.

<table>
<tbody>
<tr>
Expand Down Expand Up @@ -58,7 +54,7 @@ There are many [releases of Node](https://nodejs.org/en/blog/release/) — newer

Generally you should use the most recent _LTS (long-term supported)_ release as this will be more stable than the "current" release while still having relatively recent features (and is still being actively maintained). You should use the _Current_ release if you need a feature that is not present in the LTS version.

For _Express_ you should always use the latest version.
For _Express_ you should use the most recent LTS release of Node.

### What about databases and other dependencies?

Expand All @@ -85,11 +81,11 @@ After `nvm-windows` has installed, open a command prompt (or PowerShell) and ent
nvm install lts
```

At time of writing the LTS version of nodejs is 20.11.0.
At time of writing the LTS version of nodejs is 22.17.0.
You can set this as the _current version_ to use with the command below:

```bash
nvm use 20.11.0
nvm use 22.17.0
```

> [!NOTE]
Expand All @@ -109,12 +105,12 @@ After `nvm` has installed, open a terminal enter the following command to downlo
nvm install --lts
```

At the time of writing, the LTS version of nodejs is 20.11.0.
At the time of writing, the LTS version of nodejs is 22.17.0.
The command `nvm list` shows the downloaded set of version and the current version.
You can set a particular version as the _current version_ with the command below (the same as for `nvm-windows`)

```bash
nvm use 20.11.0
nvm use 22.17.0
```

Use the command `nvm --help` to find out other command line options.
Expand All @@ -127,14 +123,14 @@ A good way to do this is to use the "version" command in your terminal/command p

```bash
> node -v
v20.11.0
v22.17.0
```

The _Nodejs_ package manager _npm_ should also have been installed, and can be tested in the same way:

```bash
> npm -v
10.2.4
10.9.2
```

As a slightly more exciting test let's create a very basic "pure node" server that prints out "Hello World" in the browser when you visit the correct URL in your browser:
Expand Down Expand Up @@ -217,20 +213,20 @@ The following steps show how you can use npm to download a package, save it into
{
"name": "myapp",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
"license": "ISC",
"description": ""
}
```

3. Now install Express in the `myapp` directory and save it in the dependencies list of your **package.json** file:

```bash
npm install express@^4.21.2
npm install express
```

The dependencies section of your **package.json** will now appear at the end of the **package.json** file and will include _Express_.
Expand All @@ -247,7 +243,7 @@ The following steps show how you can use npm to download a package, save it into
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.21.2"
"express": "^5.1.0"
}
}
```
Expand Down Expand Up @@ -303,9 +299,9 @@ npm install eslint --save-dev
The following entry would then be added to your application's **package.json**:

```json
"devDependencies": {
"eslint": "^7.10.0"
}
"devDependencies": {
"eslint": "^9.30.1"
}
```

> [!NOTE]
Expand All @@ -318,7 +314,7 @@ In addition to defining and fetching dependencies you can also define _named_ sc
> [!NOTE]
> Task runners like [Gulp](https://gulpjs.com/) and [Grunt](https://gruntjs.com/) can also be used to run tests and other external tools.

For example, to define a script to run the _eslint_ development dependency that we specified in the previous section we might add the following script block to our **package.json** file (assuming that our application source is in a folder /src/js):
For example, to define a script to run the _eslint_ development dependency that we specified in the previous section we might add the following script block to our **package.json** file (assuming that our application source is in a folder `/src/js`):

```json
"scripts": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ The author detail page needs to display the information about the specified `Aut

Open **/controllers/authorController.js**.

Add the following lines to the top of the file to `require()` the `Book` module needed by the author detail page (other modules such as "express-async-handler" should already be present).
Add the following lines to the top of the file to `require()` the `Book` module needed by the author detail page.

```js
const Book = require("../models/book");
Expand All @@ -21,7 +21,7 @@ Find the exported `author_detail()` controller method and replace it with the fo

```js
// Display detail page for a specific Author.
exports.author_detail = asyncHandler(async (req, res, next) => {
exports.author_detail = async (req, res, next) => {
// Get details of author and all their books (in parallel)
const [author, allBooksByAuthor] = await Promise.all([
Author.findById(req.params.id).exec(),
Expand All @@ -40,12 +40,12 @@ exports.author_detail = asyncHandler(async (req, res, next) => {
author,
author_books: allBooksByAuthor,
});
});
};
```

The approach is exactly the same as described for the [Genre detail page](/en-US/docs/Learn_web_development/Extensions/Server-side/Express_Nodejs/Displaying_data/Genre_detail_page).
The route controller function uses `Promise.all()` to query the specified `Author` and their associated `Book` instances in parallel.
If no matching author is found an Error object is sent to the Express error handling middleware.
If no matching author is found an `Error` object is sent to the Express error handling middleware.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
If no matching author is found an `Error` object is sent to the Express error handling middleware.
If no matching author is found, an `Error` object is sent to the Express error handling middleware.

If the author is found then the retrieved database information is rendered using the "author_detail" template.

## View
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ Open **/controllers/authorController.js**. Find the exported `author_list()` con

```js
// Display list of all Authors.
exports.author_list = asyncHandler(async (req, res, next) => {
exports.author_list = async (req, res, next) => {
const allAuthors = await Author.find().sort({ family_name: 1 }).exec();
res.render("author_list", {
title: "Author List",
author_list: allAuthors,
});
});
};
```

The route controller function follows the same pattern as for the other list pages.
Expand Down Expand Up @@ -74,7 +74,7 @@ The genre list controller function needs to get a list of all `Genre` instances,
- Sort the results by name, in ascending order.

3. The template to be rendered should be named **genre_list.pug**.
4. The template to be rendered should be passed the variables `title` ('Genre List') and `genre_list` (the list of genres returned from your `Genre.find()` callback).
4. The template to be rendered should be passed the variables `title` ('Genre List') and `genre_list` (the list of genres returned from `Genre.find()`).
5. The view should match the screenshot/requirements above (this should have a very similar structure/format to the Author list view, except for the fact that genres do not have dates).

## Next steps
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Open **/controllers/bookController.js**. Find the exported `book_detail()` contr

```js
// Display detail page for a specific book.
exports.book_detail = asyncHandler(async (req, res, next) => {
exports.book_detail = async (req, res, next) => {
// Get details of books, book instances for specific book
const [book, bookInstances] = await Promise.all([
Book.findById(req.params.id).populate("author").populate("genre").exec(),
Expand All @@ -32,15 +32,15 @@ exports.book_detail = asyncHandler(async (req, res, next) => {
book,
book_instances: bookInstances,
});
});
};
```

> [!NOTE]
> We don't need to require any additional modules in this step, as we already imported the dependencies when we implemented the home page controller.
The approach is exactly the same as described for the [Genre detail page](/en-US/docs/Learn_web_development/Extensions/Server-side/Express_Nodejs/Displaying_data/Genre_detail_page).
The route controller function uses `Promise.all()` to query the specified `Book` and its associated copies (`BookInstance`) in parallel.
If no matching book is found an Error object is returned with a "404: Not Found" error.
If no matching book is found an `Error` object is returned with a "404: Not Found" error.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
If no matching book is found an `Error` object is returned with a "404: Not Found" error.
If no matching book is found, an `Error` object is returned with a "404: Not Found" error.

If the book is found, then the retrieved database information is rendered using the "book_detail" template.
Since the key 'title' is used to give name to the webpage (as defined in the header in 'layout.pug'), this time we are passing `results.book.title` while rendering the webpage.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ Open **/controllers/bookController.js**. Find the exported `book_list()` control

```js
// Display list of all books.
exports.book_list = asyncHandler(async (req, res, next) => {
exports.book_list = async (req, res, next) => {
const allBooks = await Book.find({}, "title author")
.sort({ title: 1 })
.populate("author")
.exec();

res.render("book_list", { title: "Book List", book_list: allBooks });
});
};
```

The route handler calls the `find()` function on the `Book` model, selecting to return only the `title` and `author` as we don't need the other fields (it will also return the `_id` and virtual fields), and sorting the results by the title alphabetically using the `sort()` method.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Find the exported `bookinstance_detail()` controller method and replace it with

```js
// Display detail page for a specific BookInstance.
exports.bookinstance_detail = asyncHandler(async (req, res, next) => {
exports.bookinstance_detail = async (req, res, next) => {
const bookInstance = await BookInstance.findById(req.params.id)
.populate("book")
.exec();
Expand All @@ -32,7 +32,7 @@ exports.bookinstance_detail = asyncHandler(async (req, res, next) => {
title: "Book:",
bookinstance: bookInstance,
});
});
};
```

The implementation is very similar to that used for the other model detail pages.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ Find the exported `bookinstance_list()` controller method and replace it with th

```js
// Display list of all BookInstances.
exports.bookinstance_list = asyncHandler(async (req, res, next) => {
exports.bookinstance_list = async (req, res, next) => {
const allBookInstances = await BookInstance.find().populate("book").exec();

res.render("bookinstance_list", {
title: "Book Instance List",
bookinstance_list: allBookInstances,
});
});
};
```

The route handler calls the `find()` function on the `BookInstance` model, and then daisy-chains a call to `populate()` with the `book` field—this will replace the book id stored for each `BookInstance` with a full `Book` document.
Expand Down Expand Up @@ -62,7 +62,7 @@ block content
p There are no book copies in this library.
```

This view is much the same as all the others. It extends the layout, replacing the _content_ block, displays the `title` passed in from the controller, and iterates through all the book copies in `bookinstance_list`. For each copy we display its status (color coded) and if the book is not available, its expected return date. One new feature is introduced—we can use dot notation after a tag to assign a class. So `span.text-success` will be compiled to `<span class="text-success">` (and might also be written in Pug as `span(class="text-success")`.
This view is much the same as all the others. It extends the layout, replacing the _content_ block, displays the `title` passed in from the controller, and iterates through all the book copies in `bookinstance_list`. For each copy we display its status (color coded) and if the book is not available, its expected return date. One new feature is introduced—we can use dot notation after a tag to assign a class. So `span.text-success` will be compiled to `<span class="text-success">` (and might also be written in Pug as `span(class="text-success")`).

## What does it look like?

Expand Down
Loading