Skip to content

Commit aa82a74

Browse files
committed
Upgrade Express from v4 to v5
1 parent 6334eb7 commit aa82a74

File tree

6 files changed

+157
-286
lines changed

6 files changed

+157
-286
lines changed

src/content/0/en/part0a.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,7 @@ Despite changes *all the submitted exercises remain valid*, and the course can b
332332
333333
Recent major changes
334334
335+
- Part 4 (13th August, 2025): Express updated to version 5 and the express-async-errors library removed from part 4b
335336
- Part 9 (28th August, 2024): Zod library for validating request body type
336337
- Part 3 (20th June, 2024): ESLint configurations updated
337338
- Part 10 (21st March, 2024): Create React app replaced with Vite

src/content/0/fi/osa0a.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,7 @@ Kurssilla ei ole enää vuosittaisia versiota. Kurssi on siis käynnissä koko a
275275
Muutoksista huolimatta <i>kaikki jo tehdyt palautukset säilyvät voimassa</i>, eli voit jatkaa kurssia päivityksistä huolimatta normaaliin tapaan.
276276

277277
Viimeaikaisia isompia muutoksia
278+
- Osa 4 (13.8.2025): Express päivitetty versioon 5 ja kirjasto express-async-errors poistettu osasta 4b
278279
- Osa 9 (28.8.2024): Zod-kirjasto datan validointiin
279280
- Osa 3 (20.6.2024): ESLint-konfiguraatiot päivitetty
280281
- Osa 12 (21.3.2024): Create React app korvattu Vitellä

src/content/4/en/part4.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ lang: en
88

99
In this part, we will continue our work on the backend. Our first major theme will be writing unit and integration tests for the backend. After we have covered testing, we will take a look at implementing user authentication and authorization.
1010

11-
<i>Part updated 28th May 2025</i>
11+
<i>Part updated 13th August 2025</i>
1212
- <i>Node updated to version v22.3.0</i>
13+
- <i>Express updated to version 5 and the express-async-errors library removed from part 4b</i>
1314
- <i>Small fixes and improvements</i>
1415

1516
</div>

src/content/4/en/part4b.md

Lines changed: 75 additions & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,8 @@ test('all notes are returned', async () => {
330330

331331
```
332332

333+
You can find the code for our current application in its entirety in the <i>part4-3</i> branch of [this GitHub repository](https://github.com/fullstack-hy2020/part3-notes-backend/tree/part4-3).
334+
333335
### Running tests one by one
334336

335337
The _npm test_ command executes all of the tests for the application. When we are writing tests, it is usually wise to only execute one or two tests.
@@ -459,9 +461,19 @@ The code declares that the function assigned to _main_ is asynchronous. After th
459461

460462
### async/await in the backend
461463

462-
Let's start to change the backend to async and await. As all of the asynchronous operations are currently done inside of a function, it is enough to change the route handler functions into async functions.
464+
Let's start to change the backend to async and await. Let's start with the route responsible for fetching all notes.
465+
466+
As all of the asynchronous operations are currently done inside of a function, it is enough to change the route handler functions into async functions. The route for fetching all notes
467+
468+
```js
469+
notesRouter.get('/', (request, response) => {
470+
Note.find({}).then((notes) => {
471+
response.json(notes)
472+
})
473+
})
474+
```
463475

464-
The route for fetching all notes gets changed to the following:
476+
gets changed to the following:
465477

466478
```js
467479
notesRouter.get('/', async (request, response) => {
@@ -472,9 +484,7 @@ notesRouter.get('/', async (request, response) => {
472484

473485
We can verify that our refactoring was successful by testing the endpoint through the browser and by running the tests that we wrote earlier.
474486

475-
You can find the code for our current application in its entirety in the <i>part4-3</i> branch of [this GitHub repository](https://github.com/fullstack-hy2020/part3-notes-backend/tree/part4-3).
476-
477-
### More tests and refactoring the backend
487+
### Refactoring the route responsible for adding a note
478488

479489
When code gets refactored, there is always the risk of [regression](https://en.wikipedia.org/wiki/Regression_testing), meaning that existing functionality may break. Let's refactor the remaining operations by first writing a test for each route of the API.
480490

@@ -667,58 +677,68 @@ after(async () => {
667677

668678
The code using promises works and the tests pass. We are ready to refactor our code to use the async/await syntax.
669679

670-
We make the following changes to the code that takes care of adding a new note (notice that the route handler definition is preceded by the _async_ keyword):
680+
The route responsible for adding a new note
671681

672682
```js
673-
notesRouter.post('/', async (request, response, next) => {
683+
notesRouter.post('/', (request, response, next) => {
674684
const body = request.body
675685

676686
const note = new Note({
677687
content: body.content,
678688
important: body.important || false,
679689
})
680690

681-
const savedNote = await note.save()
682-
response.status(201).json(savedNote)
691+
note
692+
.save()
693+
.then((savedNote) => {
694+
response.status(201).json(savedNote)
695+
})
696+
.catch((error) => next(error))
683697
})
684698
```
685699

686-
There's a slight problem with our code: we don't handle error situations. How should we deal with them?
687-
688-
### Error handling and async/await
689-
690-
If there's an exception while handling the POST request we end up in a familiar situation:
691-
692-
![terminal showing unhandled promise rejection warning](../../images/4/6.png)
693-
694-
In other words, we end up with an unhandled promise rejection, and the request never receives a response.
695-
696-
With async/await the recommended way of dealing with exceptions is the old and familiar _try/catch_ mechanism:
700+
changes as follows:
697701

698702
```js
699-
notesRouter.post('/', async (request, response, next) => {
703+
notesRouter.post('/', async (request, response) => { // highlight-line
700704
const body = request.body
701705

702706
const note = new Note({
703707
content: body.content,
704708
important: body.important || false,
705709
})
710+
706711
// highlight-start
707-
try {
708-
const savedNote = await note.save()
709-
response.status(201).json(savedNote)
710-
} catch (exception) {
711-
next(exception)
712-
}
712+
const savedNote = await note.save()
713+
response.status(201).json(savedNote)
713714
// highlight-end
714715
})
715716
```
716717

717-
The catch block simply calls the _next_ function, which passes the request handling to the error handling middleware.
718+
You need to add the _async_ keyword at the beginning of the handler to enable the use of _async/await_ syntax. The code becomes much simpler.
719+
720+
Notably, possible errors no longer need to be forwarded separately for handling. In code using promises, a possible error was passed to the error-handling middleware like this:
721+
722+
```js
723+
note
724+
.save()
725+
.then((savedNote) => {
726+
response.json(savedNote)
727+
})
728+
.catch((error) => next(error)) // highlight-line
729+
```
730+
731+
When using _async/await_ syntax, Express will [automatically call](https://expressjs.com/en/guide/error-handling.html) the error-handling middleware if an await statement throws an error or the awaited promise is rejected. This makes the final code even cleaner.
718732

719-
After making the change, all of our tests will pass once again.
733+
**Note:** This feature is available starting from Express version 5. If you installed Express as a dependency before March 31, 2025, you might still be using version 4. You can check your project's Express version in the _package.json_ file. If you have an older version, update to version 5 with the following command:
720734

721-
Next, let's write tests for fetching and removing an individual note:
735+
```bash
736+
npm install express@5
737+
```
738+
739+
### Refactoring the route responsible for fetching a single note
740+
741+
Next, let's write a test for viewing the details of a single note. The code highlights the actual API operation being performed:
722742

723743
```js
724744
test('a specific note can be viewed', async () => {
@@ -734,119 +754,56 @@ test('a specific note can be viewed', async () => {
734754

735755
assert.deepStrictEqual(resultNote.body, noteToView)
736756
})
737-
738-
test('a note can be deleted', async () => {
739-
const notesAtStart = await helper.notesInDb()
740-
const noteToDelete = notesAtStart[0]
741-
742-
// highlight-start
743-
await api
744-
.delete(`/api/notes/${noteToDelete.id}`)
745-
.expect(204)
746-
// highlight-end
747-
748-
const notesAtEnd = await helper.notesInDb()
749-
750-
const contents = notesAtEnd.map(n => n.content)
751-
assert(!contents.includes(noteToDelete.content))
752-
753-
assert.strictEqual(notesAtEnd.length, helper.initialNotes.length - 1)
754-
})
755757
```
756758

757-
Both tests share a similar structure. In the initialization phase, they fetch a note from the database. After this, the tests call the actual operation being tested, which is highlighted in the code block. Lastly, the tests verify that the outcome of the operation is as expected.
759+
First, the test fetches a single note from the database. Then, it checks that the specific note can be retrieved through the API. Finally, it verifies that the content of the fetched note is as expected.
758760

759-
There is one point worth noting in the first test. Instead of the previously used method [strictEqual](https://nodejs.org/api/assert.html#assertstrictequalactual-expected-message), the method [deepStrictEqual](https://nodejs.org/api/assert.html#assertdeepstrictequalactual-expected-message) is used:
761+
There is one point worth noting in the test. Instead of the previously used method [strictEqual](https://nodejs.org/api/assert.html#assertstrictequalactual-expected-message), the method [deepStrictEqual](https://nodejs.org/api/assert.html#assertdeepstrictequalactual-expected-message) is used:
760762

761763
```js
762764
assert.deepStrictEqual(resultNote.body, noteToView)
763765
```
764766

765767
The reason for this is that _strictEqual_ uses the method [Object.is](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is) to compare similarity, i.e. it compares whether the objects are the same. In our case, we want to check that the contents of the objects, i.e. the values of their fields, are the same. For this purpose _deepStrictEqual_ is suitable.
766768

767-
The tests pass and we can safely refactor the tested routes to use async/await:
768-
769-
```js
770-
notesRouter.get('/:id', async (request, response, next) => {
771-
try {
772-
const note = await Note.findById(request.params.id)
773-
if (note) {
774-
response.json(note)
775-
} else {
776-
response.status(404).end()
777-
}
778-
} catch (exception) {
779-
next(exception)
780-
}
781-
})
782-
```
769+
The tests pass and we can safely refactor the tested route to use async/await:
783770

784771
```js
785-
notesRouter.delete('/:id', async (request, response, next) => {
786-
try {
787-
await Note.findByIdAndDelete(request.params.id)
788-
response.status(204).end()
789-
} catch (exception) {
790-
next(exception)
772+
notesRouter.get('/:id', async (request, response) => {
773+
const note = await Note.findById(request.params.id)
774+
if (note) {
775+
response.json(note)
776+
} else {
777+
response.status(404).end()
791778
}
792779
})
793780
```
794781

795-
You can find the code for our current application in its entirety in the <i>part4-4</i> branch of [this GitHub repository](https://github.com/fullstack-hy2020/part3-notes-backend/tree/part4-4).
796-
797-
### Eliminating the try-catch
782+
### Refactoring the route responsible for deleting a note
798783

799-
Async/await unclutters the code a bit, but the 'price' is the <i>try/catch</i> structure required for catching exceptions.
800-
All of the route handlers follow the same structure
784+
Let's also add a test for the route that handles deleting a note:
801785

802786
```js
803-
try {
804-
// do the async operations here
805-
} catch (exception) {
806-
next(exception)
807-
}
808-
```
809-
810-
One starts to wonder if it would be possible to refactor the code to eliminate the <i>catch</i> from the methods?
811-
812-
The [express-async-errors](https://github.com/davidbanham/express-async-errors) library has a solution for this.
813-
814-
Let's install the library
815-
816-
```bash
817-
npm install express-async-errors
818-
```
819-
820-
Using the library is <i>very</i> easy.
821-
You introduce the library in <i>app.js</i>, _before_ you import your routes:
787+
test('a note can be deleted', async () => {
788+
const notesAtStart = await helper.notesInDb()
789+
const noteToDelete = notesAtStart[0]
822790

823-
```js
824-
require('express-async-errors') // highlight-line
825-
const express = require('express')
826-
const mongoose = require('mongoose')
827-
const config = require('./utils/config')
828-
const logger = require('./utils/logger')
829-
const middleware = require('./utils/middleware')
830-
const notesRouter = require('./controllers/notes')
791+
await api
792+
.delete(`/api/notes/${noteToDelete.id}`)
793+
.expect(204)
831794

832-
// ...
833-
```
795+
const notesAtEnd = await helper.notesInDb()
834796

835-
The 'magic' of the library allows us to eliminate the try-catch blocks completely.
836-
For example the route for deleting a note
797+
const contents = notesAtEnd.map(n => n.content)
798+
assert(!contents.includes(noteToDelete.content))
837799

838-
```js
839-
notesRouter.delete('/:id', async (request, response, next) => {
840-
try {
841-
await Note.findByIdAndDelete(request.params.id)
842-
response.status(204).end()
843-
} catch (exception) {
844-
next(exception)
845-
}
800+
assert.strictEqual(notesAtEnd.length, helper.initialNotes.length - 1)
846801
})
847802
```
848803

849-
becomes
804+
The test is structured similarly to the one that checks viewing a single note. First, a single note is fetched from the database, then its deletion via the API is tested. Finally, it is verified that the note no longer exists in the database and that the total number of notes has decreased by one.
805+
806+
The tests still pass, so we can safely proceed with refactoring the route:
850807

851808
```js
852809
notesRouter.delete('/:id', async (request, response) => {
@@ -855,33 +812,7 @@ notesRouter.delete('/:id', async (request, response) => {
855812
})
856813
```
857814

858-
Because of the library, we do not need the _next(exception)_ call anymore.
859-
The library handles everything under the hood. If an exception occurs in an <i>async</i> route, the execution is automatically passed to the error-handling middleware.
860-
861-
The other routes become:
862-
863-
```js
864-
notesRouter.get('/:id', async (request, response) => {
865-
const note = await Note.findById(request.params.id)
866-
if (note) {
867-
response.json(note)
868-
} else {
869-
response.status(404).end()
870-
}
871-
})
872-
873-
notesRouter.post('/', async (request, response) => {
874-
const body = request.body
875-
876-
const note = new Note({
877-
content: body.content,
878-
important: body.important || false,
879-
})
880-
881-
const savedNote = await note.save()
882-
response.status(201).json(savedNote)
883-
})
884-
```
815+
You can find the code for our current application in its entirety in the <i>part4-4</i> branch of [this GitHub repository](https://github.com/fullstack-hy2020/part3-notes-backend/tree/part4-4).
885816

886817
### Optimizing the beforeEach function
887818

src/content/4/fi/osa4.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ lang: fi
88

99
Jatkamme tämän osan backendin parissa. Osan ensimmäinen iso teema on backendin yksikkö- ja integraatiotestaus. Testauksen jälkeen toteutetaan backendin logiikka käyttäjienhallintaan ja kirjautumiseen.
1010

11-
<i>Osa päivitetty 28.5.2025</i>
11+
<i>Osa päivitetty 13.8.2025</i>
1212
- <i>Node päivitetty versioon v22.3.0</i>
13+
- <i>Express päivitetty versioon 5 ja kirjasto express-async-errors poistettu osasta 4b</i>
1314
- <i>Pieniä korjauksia ja parannuksia</i>
1415

1516
</div>

0 commit comments

Comments
 (0)