├── .gitignore
├── README.md
└── exercises
├── exercise-openid-connect.md
├── exercise-testing.md
└── exercise-todo-app.md
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | *.iml
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PG6301 Web Development and API design
2 |
3 | Welcome to this course in Web Development and API Design. In this course, we will
4 | look at creating single-page applications with React backed by APIs implemented
5 | with React. The application will store data in MongoDB and be deployed on Heroku
6 |
7 | * [Lectures](#lectures)
8 | * [Reference material](#course-reference-material)
9 |
10 | ## Understanding the course
11 |
12 | In this course, we expect you to become proficient at building web applications
13 | with JavaScript, React and Express. During the lectures, you will see live coding
14 | of how such applications may be constructed and many topics will be explained
15 | along the way.
16 |
17 | The course will not have slides, but all the lectures will be recorded and made
18 | available on Canvas. Each lecture will consist of 10-15 commits which will be
19 | available on GitHub for student's reference.
20 |
21 | There are many topics that we believe are more suitable for self-study than
22 | classroom explanations, and you will not always be shown how all topics are used
23 | in a more general situation. *You will be expected to master some such topics
24 | to get a top grade at the exam*. In order to be prepared for the exam, you have
25 | to follow the lectures, but you also have to be able to solve new problems and
26 | find relevant information along the way. To be able to do this, it's extremely
27 | valuable for you to follow the exercises along the lectures.
28 |
29 | The lectures will be recorded and the recordings will be available in Panopto in Canvas.
30 |
31 | ### The example applications
32 |
33 | In the course we will mainly be building two example applications:
34 |
35 | * The todo-application: This is a very common example, and you can see lots of examples
36 | using this online. The application lets to users create tasks and mark them as
37 | complete. In addition, we will be adding details to the tasks and give access to
38 | tasks to other users
39 | * Cash accounting: The teacher serves as treasurer for the local school marching band.
40 | On the annual dugnads they need to keep track of cash sales, something that most
41 | accounting software isn't good at. So we're using this chance to build a application
42 | with a real need.
43 |
44 | ## Lectures
45 |
46 | ### Lecture 1: A tour of React, Express and Heroku
47 |
48 |
49 |
50 | [Mentimenter](https://www.menti.com/alkd1oaizxmy)
51 |
52 | We explore the most important parts to the whole application up and running on
53 | a server. This lecture will be *way too fast to understand* and will serve merely
54 | as a teaser to topics that will be important through the course. After the
55 | lecture, you will only be expected to know the basics of how to create a React
56 | application with Vite and React Router
57 |
58 | * [Commit log from lecture (only available after lecture)](https://github.com/kristiania-pg6301-2024/pg6301-frontend-programming/commits/lecture/01)
59 | * [Reference implementation](https://github.com/kristiania-pg6301-2024/pg6301-frontend-programming/tree/reference/01) [deployed app](https://pg6301-reference-01-e3c9a1cd874d.herokuapp.com/)
60 | * [Exercise text](https://github.com/kristiania-pg6301-2024/pg6301-frontend-programming/blob/exercise/01/start/README.md)
61 |
62 |
63 |
64 | Material from previous years
65 |
66 | #### Material from previous years
67 |
68 | * [Commit log from live coding](https://github.com/kristiania-pg6301-2022/pg6301-react-and-express-lectures/commits/lectures/01)
69 | * [Commit log from lecture (only available after lecture)](https://github.com/kristiania-pg6301-2023/pg6301-frontend-programming/commits/lecture/01)
70 | * [Reference implementation](https://github.com/kristiania-pg6301-2023/pg6301-frontend-programming/tree/reference/01)
71 | * [Exercise text](https://github.com/kristiania-pg6301-2023/pg6301-frontend-programming/tree/exercise/01/start/README.md)
72 | * [Exercise answer](https://github.com/kristiania-pg6301-2023/pg6301-frontend-programming/commits/exercise/01/solution)
73 |
74 |
75 |
76 | ### Lecture 2: React, use state and props
77 |
78 |
79 |
80 | [Mentimenter](https://www.menti.com/al36tnmnnr2g)
81 |
82 | We will review the React topics from the last lecture: Creating a React app,
83 | creating functional components and using props, state and effects. We will
84 | also explore React Router more in depth
85 |
86 | See [Creating the frontend project](#creating-the-frontend-project) for a summary of the steps to set up the application
87 |
88 | * [Code from the lecture](https://github.com/kristiania-pg6301-2024/pg6301-frontend-programming/commits/lecture/02)
89 | * [Reference implementation](https://github.com/kristiania-pg6301-2024/pg6301-frontend-programming/tree/reference/02)
90 | * [Exercise text](https://github.com/kristiania-pg6301-2024/pg6301-frontend-programming/blob/exercise/02/start/README.md) - [Solution](https://github.com/kristiania-pg6301-2024/pg6301-frontend-programming/tree/exercise/02/solution)
91 |
92 | #### Reference material
93 |
94 | * [Fireship: React in 100 seconds](https://youtu.be/Tn6-PIqc4UM)
95 | * [Fireship: every React hook](https://youtu.be/TNhaISOUy6Q)
96 |
97 |
98 |
99 |
100 | Material from previous years
101 |
102 | * [Commit log from lecture (only available after lecture)](https://github.com/kristiania-pg6301-2023/pg6301-frontend-programming/commits/lecture/02)
103 | * [Reference implementation](https://github.com/kristiania-pg6301-2023/pg6301-frontend-programming/tree/reference/02)
104 | * [Exercise text](https://github.com/kristiania-pg6301-2023/pg6301-frontend-programming/tree/exercise/02/start/README.md)
105 | * [Exercise solution](https://github.com/kristiania-pg6301-2023/pg6301-frontend-programming/commits/exercise/02/solution)
106 |
107 |
108 |
109 | ### Lecture 3: useEffect, useRef and React Router
110 |
111 |
112 |
113 | We will continue on the React topics from the last lecture of creating components.
114 | We will use the `useEffect` and `useRef` hooks to set up interaction between our app and the DOM-objects in the browser
115 | and start to look at React Router.
116 |
117 | * [Code from the lecture](https://github.com/kristiania-pg6301-2024/pg6301-frontend-programming/commits/lecture/03)
118 | * [Reference implementation (with lecture 2)](https://github.com/kristiania-pg6301-2024/pg6301-frontend-programming/tree/reference/02)
119 | * [Exercise text - continued](https://github.com/kristiania-pg6301-2024/pg6301-frontend-programming/blob/exercise/03/start/README.md) - [Solution](https://github.com/kristiania-pg6301-2024/pg6301-frontend-programming/tree/exercise/02/solution)
120 |
121 | #### Reference material
122 |
123 | * [Fireship: every React hook](https://youtu.be/TNhaISOUy6Q)
124 |
125 |
126 |
127 | ### Lecture 4: Implementing a React backend on Express
128 |
129 |
130 |
131 | [Mentimenter](https://www.menti.com/alax91fi8cus)
132 |
133 | We will create an Express server which serves a React application that uses an API implemented in Express to implement
134 | functionality.
135 | See [Convert to serve from Express](#implement-server-side-apis-with-express) on the steps to take the code from the
136 | previous lecture to be served from Express.
137 |
138 | We will look at routing in Express and user interaction and error handling in React.
139 |
140 | * [Code from the lecture](https://github.com/kristiania-pg6301-2024/pg6301-frontend-programming/commits/lecture/04)
141 | * [Reference implementation](https://github.com/kristiania-pg6301-2024/pg6301-frontend-programming/tree/reference/03)
142 | * [Exercise text](https://github.com/kristiania-pg6301-2024/pg6301-frontend-programming/blob/exercise/04/start/README.md) - [Solution](https://github.com/kristiania-pg6301-2024/pg6301-frontend-programming/tree/exercise/04/solution)
143 |
144 | Reference material
145 |
146 | * [Fireship.io intro til Express](https://youtu.be/-MTSQjw5DrM)
147 |
148 |
149 |
150 | Material from previous years
151 |
152 | * [Commit log from live coding](https://github.com/kristiania-pg6301-2023/pg6301-frontend-programming/commits/lecture/03)
153 | * [Reference implementation](https://github.com/kristiania-pg6301-2023/pg6301-frontend-programming/commits/reference/03)
154 | * [Exercise text](https://github.com/kristiania-pg6301-2023/pg6301-frontend-programming/blob/exercise/04/start/README.md)
155 | * [Exercise solution](https://github.com/kristiania-pg6301-2023/pg6301-frontend-programming/commits/exercise/04/solution)
156 |
157 |
158 |
159 | ### Lecture 5: Publishing your application on Heroku
160 |
161 |
162 |
163 | In this lecture, we will upload a simple web application to a cloud service and look at automatic deploys.
164 | See [the steps to deploy to Heroku](#deploy-to-heroku)
165 |
166 | * [Code from the lecture](https://github.com/kristiania-pg6301-2024/pg6301-frontend-programming/commits/lecture/05)
167 | * [Reference implementation](https://github.com/kristiania-pg6301-2024/pg6301-frontend-programming/tree/reference/05)
168 | * [Reference implementation (quality code)](https://github.com/kristiania-pg6301-2024/pg6301-frontend-programming/tree/reference/05b)
169 | * [Exercise text](https://github.com/kristiania-pg6301-2024/pg6301-frontend-programming/blob/exercise/05/start/README.md)
170 | * [Deploying with Heroku](#deploy-to-heroku)
171 | * [Setting up quality checks](#quality-checks-with-husky-prettier-and-typescript)
172 |
173 | Reference material
174 |
175 | * [Heroku's documentation on using Node.js](https://www.heroku.com/nodejs)
176 | * [Heroku free credits for students](https://www.heroku.com/github-students)
177 |
178 | In this lecture, we also look at ways to make sure our code is good, from formatting, to linting, to testing.
179 | We will look at the tools husky, prettier and Typescript. We will also be using GitHub to run our quality
180 | checks automatically.
181 |
182 |
183 |
184 | Material from previous years
185 |
186 | * [Commit log from live coding](https://github.com/kristiania-pg6301-2023/pg6301-frontend-programming/commits/lecture/04)
187 | * [Reference implementation](https://github.com/kristiania-pg6301-2023/pg6301-frontend-programming/commits/reference/04)
188 | * [Exercise text](https://github.com/kristiania-pg6301-2023/pg6301-frontend-programming/blob/exercise/04/start/README.md)
189 | * [Exercise solution](https://github.com/kristiania-pg6301-2023/pg6301-frontend-programming/commits/exercise/04/solution)
190 |
191 | #### Material from 2022
192 |
193 | * [Commit log from live coding](https://github.com/kristiania-pg6301-2022/pg6301-react-and-express-lectures/commits/lectures/05)
194 | * [Reference implementation](https://github.com/kristiania-pg6301-2022/pg6301-react-and-express-lectures/tree/reference/05)
195 | * [Exercise answer](https://github.com/kristiania-pg6301-2022/pg6301-react-and-express-lectures/commits/exercise/answer/05)
196 |
197 |
198 |
199 | ### Lecture 6: Communication between client and server
200 |
201 |
202 |
203 | In this lecture, we will start from a blank application to review what we have covered so far. This will also give us
204 | some chance to deal with some information we have glossed over about the communication between the client and the server.
205 |
206 | We will cover:
207 |
208 | * How to deal with long-running operations using promises and `async`/`await`
209 | * Error handling
210 | * `fetch` requests
211 | * Express middleware
212 |
213 | We will set up a new project, taking advantage of what we've learned so far
214 |
215 | 1. Adding `husky` to make sure we don't commit bad code
216 | 2. Using `concurrently` to run both the client and server at once
217 | 3. Creating the client and server projects
218 | 4. Displaying a list on the client
219 | 5. Moving the information to the server
220 | 6. Handling the state when the application is loading and if an error occurs
221 | 7. Looking at GET, POST and PUT requests
222 |
223 |
224 | * [Code from the lecture](https://github.com/kristiania-pg6301-2024/pg6301-frontend-programming/commits/lecture/06)
225 | * [Reference implementation](https://github.com/kristiania-pg6301-2024/pg6301-frontend-programming/tree/reference/06)
226 |
227 | This lecture's exercise will be to get started with the assignment (see Canvas).
228 |
229 | #### Reference material
230 |
231 | * [Fireship.io video on Async/await and promises](https://www.youtube.com/watch?v=vn3tm0quoqE)
232 | * [The JavaScript Event Loop (Jake Archibald)](https://www.youtube.com/watch?v=cCOL7MC4Pl0)
233 |
234 |
235 |
236 | Material from previous years
237 |
238 | * [Commit log from live coding](https://github.com/kristiania-pg6301-2023/pg6301-frontend-programming/commits/lecture/08)
239 | * [Reference implementation](https://github.com/kristiania-pg6301-2023/pg6301-frontend-programming/commits/reference/08)
240 | * **Useful exercise**: [Move logic from client to server](https://github.com/kristiania-pg6301-2023/pg6301-frontend-programming/tree/exercise/08/start)
241 |
242 | #### Material from 2022
243 |
244 | * [Commit log from live coding 2022](https://github.com/kristiania-pg6301-2022/pg6301-react-and-express-lectures/commits/lectures/06)
245 | * [Reference implementation 2022](https://github.com/kristiania-pg6301-2022/pg6301-react-and-express-lectures/tree/reference/06)
246 | * [Exercise answer 2022](https://github.com/kristiania-pg6301-2022/pg6301-react-and-express-lectures/commits/exercise/answer/06)
247 |
248 |
249 |
250 |
251 | ### Lecture 7: Storing data MongoDB (with Typescript)
252 |
253 |
254 |
255 | In this lecture, we learn how to store and retrieve data in [MongoDB](https://www.mongodb.com/).
256 | We will also review deployment to Heroku.
257 |
258 | [Reading and writing data to MongoDB](#mongodb)
259 |
260 | * [Code from the lecture](https://github.com/kristiania-pg6301-2024/pg6301-frontend-programming/commits/lecture/07)
261 | * [Reference implementation](https://github.com/kristiania-pg6301-2024/pg6301-frontend-programming/tree/reference/07)
262 | * [Exercise text](https://github.com/kristiania-pg6301-2024/pg6301-frontend-programming/blob/exercise/07/start/README.md)
263 | * Last years exercise text contains more details: [Exercise text](https://github.com/kristiania-pg6301-2023/pg6301-frontend-programming/blob/exercise/07/start/README.md)
264 |
265 | Reference material
266 |
267 | * [MongoDB Skills](https://www.youtube.com/watch?v=0vPt7GI-2kc) - very useful and brief
268 | * [MongoDB in 100 seconds (Fireship.io)](https://www.youtube.com/watch?v=-bt_y4Loofg)
269 | * [MongoDB University: JavaScript](https://university.mongodb.com/courses/M220JS/about)
270 | * [MongoDB documentation: How to query collections](https://www.mongodb.com/docs/manual/reference/operator/query/)
271 | * [MongoDB documentation: How to insert a document](https://www.mongodb.com/docs/drivers/node/current/usage-examples/insertOne/)
272 |
273 |
274 |
275 | Material from previous years
276 |
277 | * [Commit log from live coding](https://github.com/kristiania-pg6301-2023/pg6301-frontend-programming/commits/lecture/07)
278 | * [Reference implementation](https://github.com/kristiania-pg6301-2023/pg6301-frontend-programming/commits/reference/07)
279 | * [Exercise text](https://github.com/kristiania-pg6301-2023/pg6301-frontend-programming/blob/exercise/07/start/README.md)
280 | * For the exercise solution,
281 | use [the lecture reference implementation](https://github.com/kristiania-pg6301-2023/pg6301-frontend-programming/commits/reference/07)
282 |
283 | #### Material from 2022
284 |
285 | * [Commit log from live coding](https://github.com/kristiania-pg6301-2022/pg6301-react-and-express-lectures/commits/lectures/07)
286 | * [Reference implementation](https://github.com/kristiania-pg6301-2022/pg6301-react-and-express-lectures/tree/reference/07)
287 | * [Exercise answer](https://github.com/kristiania-pg6301-2022/pg6301-react-and-express-lectures/commits/exercise/answer/07)
288 |
289 |
290 |
291 |
292 | ### Lecture 8: Software engineering with test-driven development, pair programming and continuous integration
293 |
294 |
295 |
296 | In this lecture, we will look at some popular and effective software engineering practices:
297 |
298 | * Test-driven development: Alternating between writing testing code and production code, writing the test code first
299 | * Pair-programming: Two developers working together on the same code with one keyboard and mouse, preferably alternating frequently who is at the keyboard
300 | * Refactoring: Improving the structure of the code without changing the behavior, preferably using refactoring support in the IDE
301 | * Continuous integration: Sharing the code frequently with the rest of the team, preferably running automated checks whenever the code is pushed
302 |
303 | These are some of the practices of Extreme Programming, the first Agile method to be widely documented and used. Related
304 | practices we will see are:
305 |
306 | * Coding standard: The team agreeing on things like code formatting and naming. We will enforce (some of) this with Prettier
307 | * Simple design: Only writing as much code as is needed to get the tests to pass. This relies on being able to refactor the code and verifying that it still works with automated tests
308 |
309 | Reference:
310 |
311 | * [Code from the lecture](https://github.com/kristiania-pg6301-2024/pg6301-frontend-programming/commits/lecture/08)
312 | * [Reference implementation](https://github.com/kristiania-pg6301-2024/pg6301-frontend-programming/tree/reference/08)
313 | * [Exercise text](./exercises/exercise-testing.md#exercise-8)
314 | * [Reference: Vitest](#testing)
315 | * [Reference: GitHub Actions](#deploy-to-heroku)
316 |
317 |
318 |
319 | Material from previous years
320 |
321 | * [Commit log from live coding](https://github.com/kristiania-pg6301-2023/pg6301-frontend-programming/commits/lecture/05)
322 | * [Reference implementation](https://github.com/kristiania-pg6301-2023/pg6301-frontend-programming/commits/reference/05)
323 | * [Exercise text](https://github.com/kristiania-pg6301-2023/pg6301-frontend-programming/blob/exercise/05/start/README.md)
324 | * [Exercise solution](https://github.com/kristiania-pg6301-2023/pg6301-frontend-programming/commits/exercise/05/solution)
325 |
326 | #### Material from 2022
327 |
328 | * [Commit log from live coding](https://github.com/kristiania-pg6301-2022/pg6301-react-and-express-lectures/commits/lectures/03)
329 | * [Reference implementation](https://github.com/kristiania-pg6301-2022/pg6301-react-and-express-lectures/tree/reference/03)
330 | * [Exercise answer](https://github.com/kristiania-pg6301-2022/pg6301-react-and-express-lectures/commits/exercise/answer/03)
331 |
332 |
333 |
334 | ### Lecture 9: Testing React code
335 |
336 |
337 |
338 | [Mentimenter](https://www.menti.com/almznypgkme6)
339 |
340 | In this lecture, we will look at [`@testing-library/react`](https://testing-library.com/docs/react-testing-library/intro/)
341 | for testing React applications and [Supertest](https://github.com/ladjs/supertest) for testing Express endpoints.
342 |
343 | We continue on the code from lecture 8, making sure we have some tests and that Typescript is running before we
344 | continue.
345 |
346 | * [Code from the lecture](https://github.com/kristiania-pg6301-2024/pg6301-frontend-programming/commits/lecture/09)
347 | * [Reference implementation](https://github.com/kristiania-pg6301-2024/pg6301-frontend-programming/tree/reference/09)
348 | * [Exercise text](./exercises/exercise-testing.md#exercise-9)
349 |
350 |
351 |
352 | Material from previous years
353 |
354 | * [Commit log from live coding](https://github.com/kristiania-pg6301-2023/pg6301-frontend-programming/commits/lecture/06)
355 | * Exercise text
356 | is [the same as lecture 6](https://github.com/kristiania-pg6301-2022/pg6301-react-and-express-lectures/tree/reference/03)
357 |
358 | #### Material from 2022
359 |
360 | * [Commit log from live coding](https://github.com/kristiania-pg6301-2022/pg6301-react-and-express-lectures/commits/lectures/10)
361 | * [Reference implementation](https://github.com/kristiania-pg6301-2022/pg6301-react-and-express-lectures/tree/reference/10)
362 |
363 |
364 |
365 |
366 | ### Lecture 10: Who's your user? OpenID Connect
367 |
368 |
369 |
370 | In this lecture we will implement "log in with Google"-functionality. We will also explore other identity
371 | services that also implement OpenID Connect, such as LinkedIn and Microsoft Entra ID.
372 |
373 | * [Code from the lecture](https://github.com/kristiania-pg6301-2024/pg6301-frontend-programming/commits/lecture/10)
374 | * [Reference implementation](https://github.com/kristiania-pg6301-2024/pg6301-frontend-programming/tree/reference/10)
375 | * [Exercise text](./exercises/exercise-openid-connect.md)
376 |
377 |
378 |
379 | Material from previous years
380 |
381 | * [Commit log from live coding](https://github.com/kristiania-pg6301-2023/pg6301-frontend-programming/commits/lecture/09)
382 | * [Reference implementation](https://github.com/kristiania-pg6301-2023/pg6301-frontend-programming/commits/reference/09)
383 | * [Exercise text](https://github.com/kristiania-pg6301-2023/pg6301-frontend-programming/blob/exercise/09/start/README.md)
384 |
385 | #### Material from 2022
386 |
387 | * [Commit log from live coding](https://github.com/kristiania-pg6301-2022/pg6301-react-and-express-lectures/commits/lectures/08)
388 | * [Reference implementation](https://github.com/kristiania-pg6301-2022/pg6301-react-and-express-lectures/tree/reference/08)
389 |
390 | #### Useful links
391 |
392 | * [Johannes' talk on OpenID Connect from NDC 2021](https://www.youtube.com/watch?v=CX8UfflxVMI)
393 | * [Google Developer Console](https://console.cloud.google.com/apis/credentials)
394 | * [Google Authentication documentation](https://developers.google.com/identity/protocols/oauth2#clientside)
395 |
396 |
397 |
398 | ### Lecture 11: Testing Express code
399 |
400 |
401 |
402 | In this lecture, we will look at [Supertest](https://github.com/ladjs/supertest) for testing Express endpoints.
403 |
404 | We continue on the code from lecture 9, making sure we have some tests and that Typescript is running before we
405 | continue.
406 |
407 | * [Code from the lecture](https://github.com/kristiania-pg6301-2024/pg6301-frontend-programming/commits/lecture/11)
408 | * [Reference implementation](https://github.com/kristiania-pg6301-2024/pg6301-frontend-programming/tree/reference/09)
409 | * [Exercise text](./exercises/exercise-testing.md#exercise-9)
410 |
411 |
412 |
413 |
414 |
415 | ### Lecture 12: Open ID Connect revisited
416 |
417 |
418 |
419 | * [Code from the lecture](https://github.com/kristiania-pg6301-2024/pg6301-frontend-programming/commits/lecture/12)
420 | * [Reference implementation](https://github.com/kristiania-pg6301-2024/pg6301-frontend-programming/commits/reference/12)
421 |
422 |
423 |
424 | Extra lecture from 2023 on OpenID Connect
425 |
426 | In this lecture, I will demonstrate how to set up an already created OpenID Connect server with Active Directory, then
427 | implement the necessary steps using another ID-provider, so the exact code is left as an exercise
428 |
429 | * [Starting point for lecture](https://github.com/kristiania-pg6301-2023/pg6301-frontend-programming/commits/reference/11)
430 | * [Commit log from live coding](https://github.com/kristiania-pg6301-2023/pg6301-frontend-programming/commits/lecture/11)
431 |
432 | #### Material from 2022
433 |
434 | * [Commit log from live coding](https://github.com/kristiania-pg6301-2022/pg6301-react-and-express-lectures/commits/lectures/11)
435 | * [Reference implementation](https://github.com/kristiania-pg6301-2022/pg6301-react-and-express-lectures/tree/reference/11)
436 |
437 |
438 |
439 | ## Course reference material
440 |
441 | The target for the course is a project with a frontend in React and a backend in Express. These instructions
442 | show how to create it from scratch.
443 |
444 | After all the steps, you will have a resulting structure that looks something like this:
445 |
446 | ```
447 | /
448 | client/
449 | dist/ # The output from the build process - generated by vite (add to .gitignore)
450 | node_modules/ # The local copy of dependencies - generated by npm (add to .gitignore)
451 | src/main.jsx # The starting point for React
452 | package.json # Contains scripts to run and dependencies
453 | index.html # The starting point for the client code
454 | vite.config.js # Configuration for Vite, contains React plugin and proxy settings
455 | server/
456 | node_modules/ # The local copy of dependencies - generated by npm (add to .gitignore)
457 | package.json # Contains scripts to run and dependencies
458 | server.js # The starting point for the server
459 | node_modules/ # The local copy of dependencies - generated by npm (add to .gitignore)
460 | package.json # Scripts to run both client and server in combination
461 | ```
462 |
463 | ### Creating the frontend project
464 |
465 |
466 |
467 | 1. Create a new directory. In IntelliJ, you can use File > New > Project. I recommend creating an Empty project
468 | 2. When creating a project, make sure you add `node_modules` and `dist` to `.gitignore`
469 | 3. Create a subdirectory for the client (`mkdir client`)
470 | 4. In the client directory, create the `package.json` file and add dependencies with the following commands
471 | 1. `cd client`
472 | 2. `npm install --save-dev vite`
473 | 3. `npm install react react-dom react-router-dom`
474 | 5. Set up the "dev" command to run vite
475 | * `npm pkg set scripts.dev="vite"`
476 | 6. You can now run `npm run dev`, although this will fail until you create an index.html-file (next step)
477 |
478 | ### Creating the initial React application files
479 |
480 | 1. Create a minimal HTML file as `client/index.html`. This is the essence:
481 | ```html
482 |
483 |
484 |
485 |
486 |
487 |
488 | ```
489 | 2. Create a minimal `src/main.jsx`:
490 | ```jsx
491 | import React from "react";
492 | import ReactDOM from "react-dom/client";
493 |
494 | const root = ReactDOM.createRoot(document.getElementById("root"));
495 | root.render(
Hello React
);
496 | ```
497 |
498 | You can now start working with React. Start by replacing `
Hello React
` with your own component. For example:
499 |
500 | ```jsx
501 | // ... continued from above - replace the `root.render(...)` line
502 | root.render();
503 |
504 | function Application() {
505 | const [counter, setCounter] = useState(0);
506 | return <>
507 |
;
560 | }
561 | ```
562 |
563 |
564 |
565 |
566 | ### Implement server side APIs with Express
567 |
568 |
569 |
570 | 1. Create a subdirectory on the top level (next to the `client` directory): `mkdir server`
571 | 2. In the server directory, create the `package.json` file and add dependencies with the following commands:
572 | 1. `cd server`
573 | 2. `npm install --save-dev nodemon`
574 | 3. `npm install express`
575 | 3. Set up the "dev" command to run express
576 | * `npm pkg set scripts.dev="nodemon server.js"`
577 | 4. Set the property `type` to `module` in `package.json`
578 | * `npm pkg set type="module"`
579 | 5. You can now run `npm run dev` in the server directory, although this will fail until you create a server.js-file
580 | (next step)
581 | 6. In your `client` directory, create or update a `vite.config.js` file to forward requests for `/api` from Vite to Express (see below)
582 |
583 | #### Create a minimal `server.js`
584 |
585 | This file start Express on port 3000. After you execute `npm run dev`,
586 | you can access it at http://localhost:3000
587 |
588 | ```js
589 | import express from "express";
590 |
591 | const app = express();
592 | express.use(express.static("../client/dist"));
593 | app.listen(3000);
594 | ```
595 |
596 | #### Setup `client/vite.config.js` to proxy `/api` to express
597 |
598 | ```js
599 | import {defineConfig} from "vite";
600 |
601 | export default defineConfig({
602 | server: {
603 | proxy: {
604 | "/api": "http://localhost:3000"
605 | }
606 | }
607 | });
608 | ```
609 |
610 |
611 |
612 | #### Create an API in `server.js`
613 |
614 |
615 |
616 | ```js
617 | export const moviesApi = new express.Router();
618 | moviesApi.get("/api/movies", (req, res) => {
619 | res.send([
620 | { title: "Oppenheimer" },
621 | { title: "Barbie" },
622 | ])
623 | });
624 |
625 | app.use(moviesApi);
626 | ```
627 |
628 | You can now access this API at http://localhost:3000/api/movies
629 |
630 |
631 |
632 | #### Read data from an API in React
633 |
634 |
635 |
636 | ```js
637 | function ListMovies() {
638 | const [movies, setMovies] = useState([]);
639 |
640 | async function loadMovies() {
641 | const res = await fetch("/api/movies");
642 | setMovies(await res.json());
643 | }
644 |
645 | useEffect(() => {
646 | loadMovies();
647 | }, []);
648 |
649 | return (
650 | <>
651 |
Movies
652 | {movies.map((m) => (
653 |
{m.title}
654 | ))}
655 | >
656 | );
657 | }
658 | ```
659 |
660 |
661 |
662 | #### The useLoading hook
663 |
664 | TODO: Replace with Suspense?
665 |
666 |
667 |
668 | ```javascript
669 | export function useLoading(loadingFunction, deps = []) {
670 | const [loading, setLoading] = useState(true);
671 | const [data, setData] = useState();
672 | const [error, setError] = useState();
673 |
674 | async function load() {
675 | setLoading(true);
676 | setData(undefined);
677 | setError(undefined);
678 | try {
679 | setData(await loadingFunction());
680 | } catch (error) {
681 | setError(error);
682 | } finally {
683 | setLoading(false);
684 | }
685 | }
686 |
687 | useEffect(load, deps);
688 | return { loading, data, error };
689 | }
690 | ```
691 |
692 |
693 |
694 | #### Posting data to server
695 |
696 |
697 |
698 | Expose an API from Express (in `server/`):
699 |
700 | ```js
701 | const MOVIES = [];
702 | export const moviesApi = new express.Router();
703 | moviesApi.post("/api/movies", (req, res) => {
704 | const { title } = req.body;
705 | MOVIES.push({ title, id: MOVIES.length });
706 | res.sendStatus(204);
707 | });
708 |
709 | app.use(express.json());
710 | app.use(moviesApi);
711 | ```
712 |
713 | Post JSON from React:
714 |
715 | ```jsx
716 | function AddMovieForm() {
717 | const [title, setTitle] = useState("");
718 |
719 | async function saveMovie(e) {
720 | e.preventDefault();
721 | await fetch("/api/movies", {
722 | method: "POST",
723 | body: JSON.stringify({ title }),
724 | headers: {
725 | "Content-Type": "application/json",
726 | },
727 | });
728 | }
729 |
730 | return (
731 |
742 | );
743 | }
744 | ```
745 |
746 |
747 |
748 | #### Express middleware for dealing with BrowserRouter
749 |
750 |
751 |
752 | When you use `` in React, the server must be prepared for unknown URLs. When the user
753 | reloads the browser, the browser will request URLs that are intended to be resolved on the client.
754 | The following defaults unknown requests to return `index.html`.
755 |
756 | ```javascript
757 | app.use((req, res, next) => {
758 | if (req.method === "GET") {
759 | // TODO: We probably should return 404 instead of index.html for api-calls as well
760 | res.sendFile(path.resolve("../client/dist/index.html"));
761 | } else {
762 | // try other alternative Express actions, or return 404 if none match
763 | next();
764 | }
765 | });
766 | ```
767 |
768 |
769 |
770 | ### Making the top level project work smoother
771 |
772 |
773 |
774 | With the instructions above, you have to use two terminal windows, one for client and one for server.
775 | You can set up the top level directory above `client` and `server` to run both concurrently:
776 |
777 | 1. Execute the following in the top level directory (above `client` and `server`)
778 | 2. Make `npm run dev` at top level run the same command in both subdirectories concurrently
779 | 1. `npm install --save-dev concurrently`
780 | 2. `npm pkg set scripts.dev="concurrently npm:dev:client npm:dev:server"`
781 | 3. `npm pkg set scripts.dev:client="cd client && npm run dev"`
782 | 4. `npm pkg set scripts.dev:server="cd server && npm run dev"`
783 |
784 |
785 |
786 | ### Deploy to Heroku
787 |
788 |
789 |
790 | Heroku is a cloud based Platform-as-a-Service (PaaS) that is extremely easy to use to host Node applications, like our
791 | Express server. They require paying for deployments, but
792 | with [GitHub Student Developer Pack](https://education.github.com/pack)
793 | you [get credits to use Heroku for free](https://www.heroku.com/github-students)
794 |
795 | For more information on deploying with Heroku Git (instead of GitHub),
796 | see [Deploying with Git | Heroku Dev](https://devcenter.heroku.com/articles/git).
797 |
798 | 1. In the root project make sure `npm install` is run at `postinstall`
799 | * `npm pkg set scripts.postinstall="npm run install:client && npm run install:server"`
800 | * `npm pkg set scripts.install:client="cd client && npm install --include=dev"`
801 | * `npm pkg set scripts.install:server="cd server && npm install"`
802 | 2. In the root project, define `npm run build` and `npm start`
803 | * `npm pkg set scripts.build="npm run build:client"`
804 | * `npm pkg set scripts.build:client="cd client && npm run build"`
805 | * `npm pkg set scripts.start="cd server && npm start"`
806 | 3. In the client project, define `npm run build`
807 | * `cd client`
808 | * `npm pkg set scripts.build="vite build"`
809 | * `cd ..`
810 | 4. In the server project, define `npm start`
811 | * `cd server`
812 | * `npm pkg set scripts.start="node server.js"`
813 | > Note: If you're using Typescript, this should be `ts-node server.ts` instead
814 | 5. In the server project, update `server.js` to let Heroku inject the server port as an environment variable:
815 | ```js
816 | app.listen(process.env.PORT || 3000);
817 | ```
818 | 6. In the server application, verify that `express` is configured to return the React code:
819 | ```js
820 | app.use(express.static("../client/dist"));
821 | ```
822 | 7. Create an application and configure to deploy to heroku
823 | 1. Sign up at the [Heroku Dashboard](https://dashboard.heroku.com/apps/)
824 | 2. [Create a new Heroku app](https://dashboard.heroku.com/new-app)
825 | 3. Under Deployment for your new app, select Heroku Git as Deployment Method
826 | 8. Download the [Heroku CLI](https://devcenter.heroku.com/articles/heroku-command-line)
827 | 9. From the command line, push your repository to Heroku
828 | 1. `heroku login`
829 | 2. `heroku git:remote -a `
830 | 3. `git push heroku`
831 | 4. `heroku open` (optional: opens a web browser to your Heroku application)
832 | 5. `heroku logs --tail` (optional): See the logs from Heroku in your console
833 | 10. You can see the deployment log under Activity in the Heroku Dashboard for your app and the runtime log under More > View logs
834 |
835 | **Common problems:**
836 |
837 | * "Buildpack not found"
838 | * Make sure that you have a **top-level** `package.json`-file
839 | * `express` not found
840 | * Make sure that the top level `package.json` has a script for `postinstall` which calls `npm install` in
841 | the `client` and `server` directories
842 | * `sh: 1: vite: not found`
843 | * When running on Heroku, the environment variable `NODE_ENV=production` is set.
844 | This makes `devDependencies` excluded on `npm install`. Make sure that the client install command runs
845 | as `npm install --include=dev`
846 | * The application crashes
847 | * View the log from the command line with `heroku logs --tail`
848 | * Make sure that you have a top level `start` script which calls `cd server && npm start`
849 | * The application shows an empty page or a 404 warning
850 | * Make sure that Express is set up to serve the React code: `app.use(express.static("../client/dist"));`
851 |
852 |
853 |
854 | ### Mongodb
855 |
856 | #### Reading data from MongoDb
857 |
858 |
859 |
860 | ```js
861 | import { MongoClient } from "mongodb";
862 | import dotenv from "dotenv";
863 |
864 | dotenv.config();
865 |
866 | new MongoClient(process.env.MONGODB_URL)
867 | .connect()
868 | .then((connection) => {
869 | const database = connection.db("sample_mflix");
870 | const moviesApi = express.Router();
871 | movies.get("", (req, res) => {
872 | moviesApi.get("/", async (req, res) => {
873 | const movies = await database
874 | .collection("movies")
875 | .find({ year: 2016, countries: "Norway" })
876 | .sort({ metacritic: -1 })
877 | .limit(200)
878 | .toArray();
879 | res.json(movies);
880 | });
881 | });
882 | app.use("/api/movies", moviesApi);
883 | })
884 | .catch((error) => {
885 | console.error("while connecting to MongoDB", error);
886 | });
887 | ```
888 |
889 | In this example, the database username, password and databasename is provided in `MONGODB_URL`. During local development, this value should be placed in a `server/.env`-file, which should be added to `.gitignore`.
890 |
891 | When deploying to Heroku, add `MONGODN_URL` to the application under Settings > Config Vars.
892 |
893 | When deploying to Heroku using [Atlas MongoDB](https://cloud.mongodb.com/), you also need to make sure Heroku has network access to your database under Security > Network Access. Here, you need to add `0.0.0.0/0` as an IP-address.
894 |
895 |
896 |
897 | ### Quality checks with Husky, Prettier and Typescript
898 |
899 |
900 |
901 | * Install [Husky](https://typicode.github.io/husky/) to ensure that you don't forget to fix your code before commiting
902 | * `npm install -D husky`
903 | * `npx husky init`
904 | * Creating a `npm test` task to check code
905 | * `npm test` in the root should run `npm run prettier:check && npm run test:client && npm run test:server`
906 | * `test:prettier`:
907 | * `npm install --save-dev prettier`
908 | * `npm pkg set scripts.prettier:check="prettier --check ."`
909 | * `test:client` and `test:server` should run `npm test` in the `client` and `server` directories, respectively
910 | * `npm pkg set scripts.test:client="cd client && npm test"`
911 | * `npm pkg set scripts.test:server="cd server && npm test"`
912 | * `client` directory should add typescript for `npm test`:
913 | * `cd client`
914 | * `npm install typescript`
915 | * `npx tsc --init --jsx react`
916 | * `npm pkg set scripts.test="tsc --noEmit"`
917 | * You must convert at least one file to Typescript or tsc will fail
918 | * `server` directory should add typescript for `npm test`:
919 | * `cd server`
920 | * `npm install typescript`
921 | * `npx tsc --init`
922 | * `npm pkg set scripts.test="tsc --noEmit"`
923 | * You must convert at least one file to Typescript or tsc will fail
924 |
925 |
926 |
927 | ### Testing
928 |
929 | This course uses the [Vitest](https://vitest.dev) testing library.
930 |
931 | #### Installing
932 |
933 | Installing Vitest is described on [the Vitest homepage](https://vitest.dev/)
934 |
935 |
936 |
937 | 1. `npm install --save-dev vitest`
938 | 2. `npm pkg set scripts.test=vitest`
939 | 3. `npm test`
940 |
941 |
942 |
943 | #### A trivial test (failing)
944 |
945 |
946 |
947 | ```typescript
948 | import { describe, expect, it } from "vitest";
949 |
950 | function isLeapYear(number: number) {
951 | }
952 |
953 | describe("leap years", () => {
954 | it("returns false for default years", () => {
955 | expect(isLeapYear(2025)).toBe(false);
956 | });
957 | });
958 | ```
959 |
960 |
961 |
962 | #### Snapshot testing - check that a view is rendered correctly
963 |
964 | For testing react code, I recommend [@testing-library/react](https://testing-library.com/docs/react-testing-library/intro/)
965 |
966 |
967 |
968 | To test with react, install devDependencies `@testing-library/react` and `jsdom`
969 |
970 | 1. `npm install --save-dev vitest @testing-library/react jsdom`
971 | 2. Add the following to your `vite.config.js`:
972 | ```js
973 | import { defineConfig } from "vite";
974 |
975 | export default defineConfig({
976 | test: {
977 | environment: "jsdom",
978 | },
979 | });
980 | ```
981 |
982 | * Use `render` from `@testing-library/react` to instantiate components
983 | * Use `expect(RenderResult.baseElement).toMatchSnapshot()` for a test that checks that nothing has changed
984 | * Use `RenderResult.baseElement.{querySelector,querySelectorAll}` to find DOM elements to inspect in the test
985 | * You can also use [`RenderResult.findBy{Text,LabelText}`](https://testing-library.com/docs/queries/about) to find elements - this retries for up to one second
986 | * Use `fireEvent` from `@testing-library/react` to create change, submit and other events
987 | * Use [`vitest.fn()`](https://vitest.dev/guide/mocking) to create a [Mock](https://vitest.dev/guide/mocking) function that can be used to verify that an event was triggered
988 |
989 | ```javascript
990 | import { afterEach, describe, expect, it, vitest } from "vitest";
991 | import { cleanup, render } from "@testing-library/react";
992 | import React from "react";
993 |
994 | // Without this, each test will extend the web page from the previous instead of starting over
995 | afterEach(cleanup);
996 |
997 | it("matches snapshot", async () => {
998 | const app = render(
999 |
1000 | movies}/>,
1001 | ,
1002 | );
1003 | expect(app.baseElement).toMatchSnapshot();
1004 | expect(
1005 | [...app.baseElement.querySelectorAll("h3")].map(
1006 | (c) => c.textContent
1007 | ),
1008 | ).toEqual(["Barbie", "Oppenheimer"]);
1009 | });
1010 | ```
1011 |
1012 |
1013 |
1014 | #### Simulate events
1015 |
1016 |
1017 |
1018 | ```javascript
1019 | import { afterEach, describe, expect, it, vitest } from "vitest";
1020 | import { cleanup, fireEvent, render } from "@testing-library/react";
1021 | import React from "react";
1022 |
1023 | // Without this, each test will extend the web page from the previous instead of starting over
1024 | afterEach(cleanup);
1025 |
1026 | it("handles event", async () => {
1027 | const handleClick = vitest.fn();
1028 | const app = render(
1029 | ,
1030 | );
1031 | fireEvent.click(await app.findByText("Click me"));
1032 | expect(handleClick).toBeCalledWith(123);
1033 | });
1034 | ```
1035 |
1036 |
1037 |
1038 | #### Using supertest to check server side behavior
1039 |
1040 | For testing Express components, I recommend [Supertest](https://github.com/ladjs/supertest)
1041 |
1042 |
1043 |
1044 | ***Setup***:
1045 |
1046 | 1. `cd client`
1047 | 2. `npm install --save-dev vitest supertest`
1048 |
1049 | To test a bookApi defined in `server/booksApi.js` like this:
1050 |
1051 | ```javascript
1052 | import express from "express";
1053 |
1054 | export const booksApi = new express.Router();
1055 | booksApi.get(":id", (req, res) => {
1056 | // ...
1057 | });
1058 | booksApi.put(":id", (req, res) => {
1059 | // ...
1060 | });
1061 | ```
1062 |
1063 | you can use a test in `server/tests/booksApi.test.js` like this:
1064 |
1065 | ```javascript
1066 | import { beforeAll, describe, expect, it } from "vitest";
1067 | import express from "express";
1068 | import request from "supertest";
1069 | import { booksApi } from "../booksApi";
1070 |
1071 | const app = express();
1072 | app.use(bodyParser.json());
1073 | app.use(booksApi);
1074 |
1075 | describe("books api", () => {
1076 |
1077 | it("can update existing books", async () => {
1078 | const book = (await request(app).get("/2")).body;
1079 | const updated = {
1080 | ...book,
1081 | author: "Egner",
1082 | };
1083 | await request(app).put("/2").send(updated).expect(200);
1084 | await request(app)
1085 | .get("/2")
1086 | .then((response) => {
1087 | expect(response.body).toMatchObject({
1088 | id: 2,
1089 | author: "Egner",
1090 | });
1091 | });
1092 | });
1093 |
1094 | });
1095 | ```
1096 |
1097 |
1098 |
1099 | ### GitHub Actions
1100 |
1101 |
1102 |
1103 | `.github/workflows/test.yaml`:
1104 |
1105 | ```yml
1106 | name: "npm test"
1107 |
1108 | on:
1109 | push:
1110 | branches:
1111 | - main
1112 |
1113 | jobs:
1114 | test:
1115 | runs-on: ubuntu-latest
1116 |
1117 | steps:
1118 | - uses: actions/checkout@v4
1119 | - uses: actions/setup-node@v4
1120 | with:
1121 | node-version: 20.x
1122 | cache: npm
1123 | - run: npm ci
1124 | - run: npm test
1125 | ```
1126 |
1127 |
1128 |
1129 | ## WebSockets
1130 |
1131 | ### Client side:
1132 |
1133 |
1134 |
1135 | Reference material
1136 |
1137 | * [Fireship.io video on Websockets](https://www.youtube.com/watch?v=1BfCnjr_Vjg)
1138 |
1139 | ```javascript
1140 | // Connect to ws on the same host as we got the frontend (support both http/ws and https/wss)
1141 | const ws = new WebSocket(window.location.origin.replace(/^http/, "ws"));
1142 | // log out the message and destructor the contents when we receive it
1143 | ws.onmessage = (msg) => {
1144 | console.log(msg);
1145 | const { username, message, id } = JSON.parse(msg.data);
1146 | };
1147 | // send a new message
1148 | ws.send(JSON.stringify({ username: "Myself", message: "Hello" }));
1149 | ```
1150 |
1151 |
1152 |
1153 | ### Server side
1154 |
1155 |
1156 |
1157 | ```javascript
1158 |
1159 | import { WebSocketServer } from "ws";
1160 |
1161 | // Create a websocket server (noServer means that express
1162 | // will provide the listen port)
1163 | const wsServer = new WebSocketServer({ noServer: true });
1164 |
1165 | // Keep a list of all incomings connections
1166 | const sockets = [];
1167 | let messageIndex = 0;
1168 |
1169 | // Start express app
1170 | const server = app.listen(3000);
1171 |
1172 | // Handle incoming clients
1173 | server.on("upgrade", (req, socket, head) => {
1174 | // This request is not passed through the middleware chain, so
1175 | // you have to duplicate any modifications to req here
1176 | wsServer.handleUpgrade(req, socket, head, (socket) => {
1177 | sockets.push(socket);
1178 | // Set up the handling of messages from this sockets
1179 | socket.on("message", (msg) => {
1180 | // Destructor the incoming message
1181 | const { username, message } = JSON.parse(msg);
1182 | // Add fields from server side
1183 | const id = messageIndex++;
1184 | // broadcast a new message to all recipients
1185 | for (const recipient of sockets) {
1186 | recipient.send(JSON.stringify({ id, username, message }));
1187 | }
1188 | });
1189 | });
1190 | });
1191 | ```
1192 |
1193 |
1194 |
1195 | ## OpenID Connect - Log on with Google
1196 |
1197 | ### Client side (implicit flow)
1198 |
1199 |
1200 |
1201 | "Implicit flow" means that the login provider (Google) will not require a client secret to complete the authentication.
1202 | This is often not recommended, and for example Microsoft Entra ID instead uses another mechanism called PKCE, which
1203 | protects against some security risks.
1204 |
1205 | 1. Set up the application in [Google Cloud Console](https://console.cloud.google.com/apis/credentials). Create a new
1206 | OAuth client ID and select Web Application. Make sure `http://localhost:3000` is added as an Authorized JavaScript
1207 | origin and `http://localhost:3000/callback` is an authorized redirect URI
1208 | 2. To start authentication, redirect the browser (see code below)
1209 | 3. To complete the authentication, pick up the `access_token` when Google redirects the browser back (see code below)
1210 | 4. Save the `access_token` (e.g. in `localStorage`) and add as a header to all requests to backend
1211 |
1212 |
1213 |
1214 | #### Redirect the client to authenticate
1215 |
1216 |
1217 |
1218 | ```javascript
1219 | function LoginButton() {
1220 | const [authorizationUrl, setAuthorizationUrl] = useState();
1221 | async function generateAuthorizationUrl() {
1222 | // Get the location of endpoints from Google
1223 | const { authorization_endpoint } = await fetchJson(
1224 | "https://accounts.google.com/.well-known/openid-configuration"
1225 | );
1226 | // Tell Google how to perform the authentication
1227 | const parameters = {
1228 | response_type: "token",
1229 | client_id:
1230 | "",
1231 | // Tell user to come back to http://localhost:3000/login/callback when logged in
1232 | redirect_uri: window.location.origin + "/login/callback",
1233 | scope: "profile email",
1234 | };
1235 | setAuthorizationUrl(
1236 | discoveryDoc.authorization_endpoint +
1237 | "?" +
1238 | new URLSearchParams(parameters),
1239 | );
1240 | }
1241 |
1242 | useEffect(() => {
1243 | generateAuthorizationUrl();
1244 | }, []);
1245 |
1246 | return Log in with Google;
1247 | }
1248 | ```
1249 |
1250 | In the case of Entra ID, you also need
1251 | parameters `response_type: "code"`, `response_mode: "fragment"`, `code_challenge_method` and `code_challenge` (the
1252 | latest two are needed for PKCE).
1253 |
1254 | **For Entra ID: Generating `code_verifier` and `code_challenge`**
1255 |
1256 | When using Entra ID from the browser, you code must code prove that it was the same application that
1257 | started the request and completed the request. This is called Proof of Key Challenge Exchange (PKCE,
1258 | often pronounced "pixie"). You must create a random value and save it (I use `sessionStorage`).
1259 | The authorization request must contain the property `code_challenge` which is the hash of the random
1260 | value. When completing the logon with a token request (see below), your code must include the (unhashed)
1261 | random value as a property `code_verifier`.
1262 |
1263 | ```typescript
1264 | // From https://stackoverflow.com/a/75809704/27658
1265 | function randomString() {
1266 | const array = new Uint8Array(32);
1267 | crypto.getRandomValues(array);
1268 | return Array.from(array)
1269 | .map((b) => b.toString(16).padStart(2, "0"))
1270 | .join("");
1271 | }
1272 |
1273 | // URL-safe base 64 encoding is defined in https://datatracker.ietf.org/doc/html/rfc4648#page-7
1274 | function encodeBytesAsBase64Url(bytes: ArrayBuffer): string {
1275 | return btoa(
1276 | String.fromCharCode.apply(null, Array.from(new Uint8Array(bytes))),
1277 | )
1278 | .split("=")[0]
1279 | .replace(/\+/g, "-")
1280 | .replace(/\//g, "_");
1281 | }
1282 |
1283 | async function sha256hash(s: string): Promise {
1284 | return await crypto.subtle.digest("SHA-256", new TextEncoder().encode(s));
1285 | }
1286 |
1287 | const code_verifier = randomString();
1288 | sessionStorage.setItem("code_verifier", code_verifier);
1289 | const code_challenge = base64url(await sha256hash(code_verifier));
1290 | ```
1291 |
1292 |
1293 |
1294 |
1295 | #### Handle the authentication callback
1296 |
1297 |
1298 |
1299 | ```javascript
1300 | // Router should take user here on /callback
1301 | export function LoginCallback() {
1302 | const navigate = useNavigate();
1303 | // Given an URL like http://localhost:3000/callback#access_token=sdlgnsoln&foo=bar,
1304 | // window.location.hash will give the part starting with "#"
1305 | // ...substring(1) will remove the "#"
1306 | // and Object.fromEntries(new URLSearchParams(...)) will parse it into an object
1307 | // In this case, hash = { access_token: "sdlgnsoln", foo: "bar" }
1308 | const callbackParameters = Object.fromEntries(
1309 | new URLSearchParams(window.location.hash.substring(1)),
1310 | );
1311 |
1312 | async function handleCallback() {
1313 | // Get the values returned from the login provider. For Active Directory,
1314 | // this will be more complex
1315 | const { access_token } = callbackParameters;
1316 | await fetch("/api/login/accessToken", {
1317 | method: "POST",
1318 | body: JSON.stringify({ access_token }),
1319 | headers: {
1320 | "content-type": "application/json",
1321 | },
1322 | });
1323 | navigate("/");
1324 | }
1325 |
1326 | useEffect(() => {
1327 | handleCallback();
1328 | }, []);
1329 |
1330 | return
Please wait...
;
1331 | }
1332 | ```
1333 |
1334 | For Active Directory, the hash will instead include a `code`, which you will then need to send to the `token_endpoint`
1335 | along with the `client_id` and `redirect_uri` as well as `grant_type: "authorization_code"` and the `code_verifier`
1336 | value from PKCE. This call will return the `access_token`.
1337 |
1338 |
1339 |
1340 | #### Handle access_token on the backend
1341 |
1342 |
1343 |
1344 | ```javascript
1345 | app.use(async (req, res, next) => {
1346 | const { access_token } = req.signedCookies;
1347 | if (access_token) {
1348 | const { userinfo_endpoint } = await fetchJSON(
1349 | "https://accounts.google.com/.well-known/openid-configuration"
1350 | );
1351 | req.userinfo = await fetchJSON(userinfo_endpoint, {
1352 | headers: { "Authorization": `Bearer ${access_token}` },
1353 | });
1354 | }
1355 | next();
1356 | });
1357 |
1358 | app.post("/api/login", (req, res) => {
1359 | const { access_token } = req.body;
1360 | res.cookie("access_token", access_token, { signed: true });
1361 | res.sendStatus(204);
1362 | });
1363 |
1364 | app.get("/profile", (req, res) => {
1365 | if (!req.userinfo) {
1366 | res.send(401);
1367 | } else {
1368 | res.send(req.userinfo);
1369 | }
1370 | });
1371 | ```
1372 |
1373 |
1374 |
1375 | ## Tools
1376 |
1377 | ### IntellJ shortcuts
1378 |
1379 |
1380 | These are some of the most versatile keyboard shortcuts in IntelliJ. There are many more, but learning these 12 will really speed up your code
1381 |
1382 | | Shortcut (Windows) | Shortcut (Mac) | Command |
1383 | |----------------------|---------------------|--------------------------------------------|
1384 | | alt-enter | opt-enter | Show content action (quick fix) |
1385 | | ctrl-alt-shift-t | ctrl-t | Refactor this (show refactor menu) |
1386 | | alt-insert | cmd-n | New... (add some content) |
1387 | | ctrl-w | opt-up | Expand selection |
1388 | | shift-alt-f10 | ctrl-alt-r | Run.... |
1389 | | shift-alt-f9 | ctrl-alt-d | Debug.... |
1390 | | shift-f10 | ctrl-d | Rerun last.... |
1391 | | ctrl-b | cmd-b | Navigate to symbol |
1392 | | alt-j | ctrl-g | Add next match to selection (multi-cursor) |
1393 | | shift-ctrl-backspace | shift-cmd-backspace | Goto last edit location |
1394 | | shift, shift | shift, shift | Search anywhere |
1395 |
1396 | Make yourself familiar with `Refactor this` (ctrl-alt-shift-t / ctrl-t) and use it to learn the shortcut keys for your
1397 | favorite refactorings like Extract method, Rename and Inline.
1398 |
1399 |
1400 | ### Git commands
1401 |
1402 |
1403 |
1404 | | Command | Description | IntelliJ shortcut |
1405 | |--------------|------------------------------------------|-------------------------------------------|
1406 | | `git init` | Creates a new local git repo in `.git/` | VCS > Import into version control |
1407 | | `git add` | Stage files to include in next commit | (not needed) |
1408 | | `git commit` | Store your local changes in git history | ctrl-k / cmd-k |
1409 | | `git push` | Upload changes to remote repo (github) | ctrl-sh-k / cmd-sh-k |
1410 | | `git clone` | Create a local copy from remote (github) | File > New > Project from version control |
1411 | | `git pull` | Update local copy with others' changes | ctrl-t / cmd-t |
1412 | | `git log` | View change history | View > Tool Windows > Version control |
1413 |
1414 |
1415 |
1416 | ## Software and libraries used in this course:
1417 |
1418 | * [React](https://react.dev)
1419 | * [NodeJs](https://nodejs.org)
1420 | * [Vite](https://vitejs.dev/)
1421 | * [ExpressJS](https://expressjs.com/)
1422 | * [IntelliJ](https://www.jetbrains.com/idea/)
1423 | * [Heroku](https://devcenter.heroku.com/)
1424 | * [MongoDB](https://www.mongodb.com/)
1425 | * [Husky](https://typicode.github.io/husky/)
1426 | * [Prettier](https://prettier.io/)
1427 | * [Vitest](https://vitest.dev/)
1428 | * [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/)
1429 | * [Supertest](https://github.com/ladjs/supertest)
1430 | * Google Login
1431 | * Entra ID (?)
1432 |
--------------------------------------------------------------------------------
/exercises/exercise-openid-connect.md:
--------------------------------------------------------------------------------
1 | # Exercise: OpenID Connect
2 |
3 | > The purpose of this exercise is to make you familiar with the OpenID protocol for login services.
4 | > The exercise takes you through creating a "Login with Google"-function and then
5 | > "Login with your school account"
6 |
7 | > **Note**: There are libraries which attempt to simplify integration. However, I have always found these
8 | > to be harder to understand than the protocol. Everything you need to know to use the protocol, you
9 | > also need to know to use the libraries.
10 |
11 | In order to create login-functionality with an Identity Provider (like Google, LinkedIn or Entra ID),
12 | you need to go through the following steps:
13 |
14 | 1. Register your application with the identity provider. You have to register your application's url and get a
15 | `client_id` from the provider
16 | 2. Create an authorization redirect. The authorization redirect sends the user to the identity provider with a
17 | return url and the client_id (from step 1)
18 | 3. Handle the callback from the Identity Provider to your application. For some Identity Providers, this callback
19 | will contain a token that proves the user's identity, for others, it contains an authentication code you can use
20 | to get this token
21 | 4. For some Identity Providers: use the authorization code from the callback to obtain the token
22 | 5. Use the access token from the Identity Provider to get information about the user
23 |
24 | ## Exercise 10: OpenID Connect with Google
25 |
26 | 1. Set up the application in [Google Cloud Console](https://console.cloud.google.com/apis/credentials). Create a new
27 | OAuth client ID and select Web Application. Make sure `http://localhost:5173` is added as an Authorized JavaScript
28 | origin and `http://localhost:5173/login/callback` is an authorized redirect URI
29 | 2. Create a [React Application](../README.md#creating-the-frontend-project) and an
30 | [Express Backend](../README.md#implement-server-side-apis-with-express)
31 | 3. To start authentication, redirect the browser ([see code below](#generating-the-authentication-redirect))
32 | 4. To complete the authentication, post `access_token` when Google redirects the browser back to the
33 | backend ([see code below](#handle-the-callback))
34 | 5. In the backend, save the `access_token` as a cookie
35 | 6. Use the cookie to fetch user information when the client makes a request to the backend
36 |
37 | ### Generating the authentication redirect
38 |
39 | ```javascript
40 | function GoogleLoginButton() {
41 | const [authorizationUrl, setAuthorizationUrl] = useState();
42 |
43 | async function generateAuthorizationUrl() {
44 | // Get the location of endpoints from Google
45 | // Implementing `fetchJSON` is left as an exercise to the reader
46 | const {authorization_endpoint} = await fetchJson(
47 | "https://accounts.google.com/.well-known/openid-configuration"
48 | );
49 | // Tell Google how to perform the authentication
50 | const parameters = {
51 | response_type: "token",
52 | client_id:
53 | "",
54 | // Tell user to come back to http://localhost:5173/login/callback when logged in
55 | redirect_uri: window.location.origin + "/login/callback",
56 | scope: "profile email",
57 | };
58 | setAuthorizationUrl(
59 | discoveryDoc.authorization_endpoint +
60 | "?" +
61 | new URLSearchParams(parameters),
62 | );
63 | }
64 |
65 | useEffect(() => {
66 | generateAuthorizationUrl();
67 | }, []);
68 |
69 | return Log in with Google;
70 | }
71 | ```
72 |
73 | ### Handle the callback
74 |
75 | In order to call the correct code when Google completes the login and redirects the user back to you
76 | application, you should set up [React Router](../README.md#react-router) with a
77 | `} />`.
78 |
79 | The `` component should extract the `access_token` and post it to the backend:
80 |
81 | ```javascript
82 | import {useNavigate} from "react-router";
83 |
84 | export function LoginCallback() {
85 | const navigate = useNavigate();
86 | // Given an URL like http://localhost:5173/login/callback#access_token=sdlgnsoln&foo=bar,
87 | // window.location.hash will give the part starting with "#"
88 | // ...substring(1) will remove the "#"
89 | // and Object.fromEntries(new URLSearchParams(...)) will parse it into an object
90 | // In this case, hash = { access_token: "sdlgnsoln", foo: "bar" }
91 | const callbackParameters = Object.fromEntries(
92 | new URLSearchParams(window.location.hash.substring(1)),
93 | );
94 |
95 | async function handleCallback() {
96 | const {access_token} = callbackParameters;
97 | await fetch("/api/login/googleAccessToken", {
98 | method: "POST",
99 | body: JSON.stringify({access_token}),
100 | headers: {
101 | "content-type": "application/json",
102 | },
103 | });
104 | navigate("/");
105 | }
106 |
107 | useEffect(() => {
108 | handleCallback();
109 | }, []);
110 |
111 | return
Please wait...
;
112 | }
113 | ```
114 |
115 | ### Save the access token in a cookie
116 |
117 | You must set up the [Express Backend](../README.md#implement-server-side-apis-with-express). Make sure that
118 | `client/vite.config.js` is set up to proxy `/api` to `http://localhost:3000`.
119 |
120 | You must set up Express to accept `POST /api/login/googleAccessToken`:
121 |
122 | ```js
123 | import express from "express";
124 | import cookieParser from "cookie-parser";
125 |
126 | const app = express();
127 | express.use(cookieParser());
128 | express.use(express.json());
129 | express.use(express.static("../client/dist"));
130 | express.post("/api/login/googleAccessToken", (req, res) => {
131 | const {access_token} = req.body;
132 | res.cookie("googleAccessToken", access_token).sendStatus(201);
133 | })
134 | app.listen(3000);
135 | ```
136 |
137 | ### Retrieve userinfo in Express
138 |
139 | We also update Express to accept `GET /api/login` to retrieve user login information:
140 |
141 | ```javascript
142 | app.get("/api/login", async (req, res) => {
143 | const {googleAccessToken} = req.cookies;
144 | if (googleAccessToken) {
145 | // Implementing `fetchJSON` is left as an exercise to the reader
146 | const {userinfo_endpoint} = await fetchJSON(
147 | "https://accounts.google.com/.well-known/openid-configuration"
148 | );
149 | const userinfo = await fetchJSON(userinfo_endpoint, {
150 | headers: {"Authorization": `Bearer ${access_token}`},
151 | });
152 | res.json(userinfo);
153 | } else {
154 | res.send(401);
155 | }
156 | });
157 | ```
158 |
159 | ### Stitching together the parts
160 |
161 | Hopefully, you will be able to make all the parts work together:
162 |
163 | In your React `` component, you should call `GET /api/login`. If the call returns 200,
164 | display the user information returned from Google, otherwise show the ``
165 |
166 | ## Exercise 12: OpenID Connect with Microsoft Entra ID (optional)
167 |
168 | Entra ID can be used to authenticate with the Active Direct account on an organization, such as the school.
169 |
170 | Entra ID doesn't support the `response_type: "token"`, which we used with Google. Instead, we must
171 | use `response_type: "code"`. With this, instead of returning a hash with `access_token`, we will be
172 | redirect back with a parameter `code`, which we must use for a new `POST` call to get the access token.
173 |
174 | Often, `response_type: "code"` is used with the backend performing the token request. This protects the
175 | user from having the code "sniffed". Since we're doing this part in the code, Entra ID requires another
176 | mechanism to protect the user instead, namely Proof of Key Code Exchange (PKCE). This requires us to
177 | pass an additional `code_challenge` parameter to the authentication request.
178 |
179 | 1. Set up the application in the [Azure Portal](https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/CreateApplicationBlade/isMSAApp~/false):
180 | 1. Log in with your school account in the [Azure Portal](https://portal.azure.com)
181 | 2. In the panel search menu, search for [Azure Active Directory B2C](https://portal.azure.com/#create/Microsoft.AzureADB2C)
182 | 3. If you haven't done so, you need to complete "Start with an Azure free trial"
183 | 4. When you have completed creating a new tenant (under Azure Active Directory B2C), you can switch to this tenant by clicking the Gears (⚙️) icon in the menu
184 | 5. Under the left-menu, select Microsoft Entra ID > Manage > App registration
185 | 6. Here you can find Entra ID's discovery endpoint and your client_id, setup the redirect_uri values and create a client_secret
186 | 2. To start authentication, redirect the browser ([see code below](#generating-the-entra-authentication-redirect))
187 | 3. To complete the authentication, use the `code` parameter when Entra redirects the browser back to the to
188 | fetch the access token and post the access token to the backend ([see code below](#handle-the-entra-callback))
189 | 4. In the backend, save the `access_token` as a cookie
190 | 5. Use the cookie to fetch user information when the client makes a request to the backend
191 |
192 |
193 | ### Put the Entra ID configuration in a separate object
194 |
195 | ```js
196 | const entraConfig = {
197 | discoveryEndpoint: "..", // The value from
198 | client_id: "..", // The value from
199 | }
200 | ```
201 |
202 | ### Generating the Entra authentication redirect
203 |
204 | This is similar but different from Google
205 |
206 | ```javascript
207 | function EntraLoginButton() {
208 | const [authorizationUrl, setAuthorizationUrl] = useState();
209 |
210 | async function generateAuthorizationUrl() {
211 | // Get the location of endpoints from EntraID
212 | const {authorization_endpoint} = await fetchJson(
213 | entraConfig.discoveryEndpoint
214 | );
215 |
216 | const code_verifier = randomString();
217 | sessionStorage.setItem("code_verifier", code_verifier);
218 | const code_challenge = encodeBytesAsBase64Url(
219 | await sha256hash(code_verifier),
220 | );
221 | const parameters = {
222 | response_type: "code",
223 | client_id: entraConfig.client_id,
224 | redirect_uri: window.location.origin + "/login/entra/callback",
225 | scope: "profile email openid",
226 | code_challenge,
227 | code_challenge_method: "S256"
228 | };
229 | setAuthorizationUrl(
230 | discoveryDoc.authorization_endpoint +
231 | "?" +
232 | new URLSearchParams(parameters),
233 | );
234 | }
235 |
236 | useEffect(() => {
237 | generateAuthorizationUrl();
238 | }, []);
239 |
240 | return Log in with your school account;
241 | }
242 | ```
243 |
244 | See the [course notes](../README.md#redirect-the-client-to-authenticate) for the
245 | implementation of `randomString`, `encodeBytesAsBase64Url` and `sha256hash`.
246 |
247 | ### Handle the Entra callback
248 |
249 | In order to call the correct code when Entra ID completes the login and redirects the user back to you
250 | application, you should set up [React Router](../README.md#react-router) with a
251 | `} />`.
252 |
253 | The `` is similar to Google, but requires a few more steps to handle the token request
254 |
255 | ```javascript
256 | import {useNavigate} from "react-router";
257 |
258 | export function EntraLoginCallback() {
259 | const navigate = useNavigate();
260 | const callbackParameters = Object.fromEntries(
261 | new URLSearchParams(window.location.search)
262 | );
263 |
264 | async function handleCallback() {
265 | const {code} = callbackParameters;
266 | const {token_endpoint} = await fetchJson(
267 | entraConfig.discoveryEndpoint
268 | );
269 | const payload = {
270 | grant_type: "authorization_code",
271 | code,
272 | client_id: entraConfig.client_id,
273 | code_verifier: sessionStorage.getItem("code_verifier"),
274 | };
275 | const res = await fetch(tokenEndpoint, {
276 | method: "POST",
277 | body: new URLSearchParams(payload),
278 | });
279 | // Here a lot can go wrong - it's best to check res.ok and log the request
280 | const {access_token} = await res.json();
281 |
282 | await fetch("/api/login/entraAccessToken", {
283 | method: "POST",
284 | body: JSON.stringify({access_token}),
285 | headers: {
286 | "content-type": "application/json",
287 | },
288 | });
289 | navigate("/");
290 | }
291 |
292 | useEffect(() => {
293 | handleCallback();
294 | }, []);
295 |
296 | return
Please wait...
;
297 | }
298 | ```
299 |
300 | ### Save the Entra ID access_token in a Cookie
301 |
302 | Saving the access_token from Entra ID very similar to the code with Google and you can extend that code as needed.
303 |
304 | ### Discussion: `id_token` vs `access_token`
305 |
306 | If you inspected the response from the token request to Entra, you may have noticed that in addition to the
307 | `access_token`, Entra ID returned a `id_token`. This is a token on a format called Json Web Token (JWT), which is
308 | a signed payload that tells you who the user is. You can look at how this token is interpreted by pasting it on
309 | https://jwt.io.
310 |
311 | An `id_token` is a self-contained, transparent identifying token that the application can verify and extract values from.
312 | The `access_token`, on the other hand is an opaque token that can be used to authorize API calls on behalf of the user.
313 | An example of such an API call is the `userinfo`-call we performed to identify the user.
314 |
315 | If you pasted the JWT into http://jwt.io, you may have noticed that it contains more information than is returned from
316 | the Entra ID `userinfo` endpoint. So, which one should we use?
317 |
318 | * If we accept an `id_token` from the browser, we need to make sure to verify the cryptographic signature. This
319 | is a little tricky, but not impossible. A clever user could change the token to contain another users credentials.
320 | If they do this, and we forget to check the signature, the attacker can impersonate another user
321 | * Alternatively, we could send the `code` to the backend and perform the token request from Express. Since we
322 | got the `id_token` directly from Entra ID, we don't need to verify the signature. But if we do this, we need
323 | to store the user information in a Cookie and make sure that we don't accept cookies that has been tampered with.
324 | (This can be done with [signed cookies](https://expressjs.com/en/resources/middleware/cookie-parser.html))
325 | * The `access_token` doesn't need to be signed when we store it in a cookie, since we're authorizing it with
326 | Entra ID. If the user tries to tamper with the `access_token` cookie, the `userinfo` call will fail with a 401-error
327 | * The `id_token` contains an expiration time that we also should respect. However, if the user signs out from
328 | Entra ID in the browser, we aren't automatically notified of this and the `id_token` will still be valid.
329 | On the other hand, the `userinfo` call will not return 401 if we try to use the `access_token` after the user
330 | is logged out. It is possible to implement "single logout" with OpenID Connect as well, but this requires more work
331 | on our part.
332 |
333 | In conclusion, it is much less work to create a secure application using the `access_token` than the `id_token`,
334 | but the `id_token` contains information such as email address which we might need. (If all we need is a unique id
335 | for the user, the `sub` value of the `userinfo` provides this)
336 |
337 | ## Deploying to Heroku
338 |
339 | When you have developed the login-functionality, you should try to deploy the application to Heroku.
340 |
341 | 1. Create a new Heroku application and get a hostname from Heroku
342 | 2. In the configuration for [Google](https://console.cloud.google.com/apis/credentials) or
343 | [Entra ID](https://portal.azure.com/#view/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/~/Overview), you should add the callback with Heroku's hostname. E.g. if your hostname
344 | on Heroku is `myapp-abc-123.herokuapp.com`, you should register `https://myapp-abc-123.herokuapp.com/login/callback`
345 | 3. Follow the steps in the [course reference](../README.md#deploy-to-heroku) to set `postinstall`, `build` and `start`
346 | scripts to work with Heroku
347 | 4. On the [Heroku dashboard](https://dashboard.heroku.com/apps/), select the app, select Settings and add the client_id and client_secret under Config Vars
348 |
--------------------------------------------------------------------------------
/exercises/exercise-testing.md:
--------------------------------------------------------------------------------
1 | # Implementing tests in your application
2 |
3 | These exercises are meant help you learn Vitest, test-driven development, pair programming and continuous integration.
4 | They do not depend on the earlier exercises in the course
5 |
6 | ## Exercise 8
7 |
8 | ### Extreme programming with test-driven development, pair programming, refactoring and continuous integration
9 |
10 |
11 |
12 |
13 |
14 | > The purpose of this exercise is to teach you software development skills that have stood the test of time in the industry
15 |
16 |
17 |
18 |
19 | ### Overview
20 |
21 | 1. Find a partner
22 | 2. Create a new empty node project on the computer of one of you
23 | 3. Write the Leap Years kata with test driven development and pair programming
24 | 4. Share the project on GitHub and give both developers access
25 | 5. Add a GitHub Actions file that runs the test
26 | 6. Implement the Roman Numerals Kata with test driven development and pair programming
27 | 7. Verify that your tests run on GitHub
28 |
29 | ### [The Leap Years kata](https://codingdojo.org/kata/LeapYears/)
30 |
31 | This is an exercise to learn the pair-programming method called "ping pong programming",
32 | where two developers alternate quickly who's at the keyboard
33 |
34 |
35 |
36 | 1. Team up with another student
37 | 2. One person creates a new (empty) project in IntelliJ and adds `vitest`-support
38 | * `npm install -D vitest`
39 | * `npm pkg set scripts.test="vitest"`
40 | 3. Run `npm test` and see that you get an error due to no tests
41 | 4. Slide the keyboard over to the other programmer
42 | 5. Create a new file named `leapYears.test.ts` with the following contents:
43 | ```typescript
44 | import {expect, it} from "vitest";
45 |
46 | it("returns false for normal years", () => {
47 | expect(isLeapYear(2025)).toBe(false);
48 | });
49 | ```
50 | 6. See that the test process automatically runs the test which now failed with the message "`ReferenceError: isLeapYear is not defined`"
51 | 7. Press F2 (Next Highlighted Error) in the editor which selects `isLeapYear`. Press Alt-Enter and select "Create function" > "(top level)"
52 | 8. The test now fails with "`AssertionError: expected undefined to be false // Object.is equality`"
53 | 9. Slide the keyboard over to the first programmer
54 | 10. Implement `isLeapYear` in the simplest way that passes the test
55 | ```typescript
56 | function isLeapYear(number: number) {
57 | return false;
58 | }
59 | ```
60 | 11. Write a new test:
61 | ```typescript
62 | it("returns true for years divisible by four", () => {
63 | expect(isLeapYear(2024)).toBe(true);
64 | });
65 | ```
66 | 12. See the test failing before sliding the keyboard to the other developer
67 | 13. Make the test pass with as little code as possible. I suggest replacing the return statement with `return number % 4 === 0`
68 | 14. Refactor the code: Put the cursor on the parameter name `number` and press `ctrl-alt-shift-t` (`ctrl-t` on Mac). Select "Rename" and rename the `number` variable to `year`
69 | 15. Now is a good time to add the files to git:
70 | * In IntelliJ, select "Version Control" > "Create Git Repository"
71 | * Right-click on the `node_modules`-directory, Select Git > Add to .gitignore > Add to .gitignore
72 | * Right-click on the `.idea/`-directory and add this to `.gitignore` too
73 | * Press `ctrl-k` (`cmd-k` on Mac), add the files to Git and commit them
74 | 16. Write the next test: `it("returns false for years divisible by 100")`, see it fail and slide the keyboard to the first developer
75 | 17. Update `isLeapYear` so that all three tests pass. This is a good time to commit the code to git
76 | 18. Write the next test: `it("returns true for years divisible by 400")`, see it fail and slide the keyboard to the other developer
77 | 19. Update `isLeapYear` so that all four tests pass and commit the code
78 | 20. Share the project on GitHub: Git > GitHub > Share Project on GitHub
79 |
80 |
81 |
82 | ### Add prettier and husky
83 |
84 | As you have finished a natural part of the code, it's a good time to add some quality control.
85 |
86 | 1. `npm install -D husky prettier`
87 | 2. `npx prettier --write .`
88 | 3. `npm pkg set scripts.test="prettier --check . && vitest"`
89 | 4. Commit the code and push it to GitHub
90 |
91 | In IntelliJ, open `package.json`, right-click and select "Apply Prettier code style rules" to make the IntelliJ
92 | automatically format the code with Prettier as you write it.
93 |
94 | ### Running tests with GitHub Actions
95 |
96 | In addition to running the tests when the code is committed, it's a good safety measure to run it on GitHub.
97 | [See the course reference material](https://github.com/kristiania-pg6301-2024/pg6301-frontend-programming/#github-actions)
98 | on the contents on a `.github/workflows/test.yaml`-file that defines a GitHub Actions Workflow that runs the
99 | tests automatically when code is pushed.
100 |
101 | Make sure that the tests run correctly on GitHub.
102 |
103 | ### [The Roman Numerals kata](https://codingdojo.org/kata/RomanNumerals/)
104 |
105 | Now that you have a project with Vitest, husky, prettier and GitHub Actions, it's a good time to write some real tests.
106 | The Roman Numerals kata is a good exercise to reinforce the pair programming and test-driven development work you
107 | learned with the Leap Years kata.
108 |
109 |
110 |
111 | 1. The first programmer creates a new test file named `romanNumerals.test.ts`. A first test can look like this:
112 | ```typescript
113 | import {expect, it} from "vitest";
114 |
115 | function toRoman(number: number) {
116 |
117 | }
118 |
119 | it("translates 1 to I", () => {
120 | expect(toRoman(1)).toBe("I");
121 | });
122 | ```
123 | 2. Make sure you see the test run and fail with the `npm test` command
124 | 3. Slide the keyboard to the other developer
125 | 4. Make the easiest implementation that will pass the test (`return "I";`)
126 | 5. After seeing the test pass, refactor with `alt-ctrl-shift-t` (Mac: `ctrl-t`): *Move...* `toRoman` to a new file and consider renaming the `number` parameter
127 | 6. Commit the code
128 | 7. Create a new test showing that `it("translates 2 to II")`
129 | 8. Slide the keyboard to the first developer to implement
130 | 9. The simplest implementation is to put an initial `if`-check
131 | 10. After implementing for 2, continue with 3 (should be `III`). After the simple implementation, refactor to use a loop
132 | 11. Continue with 4 (should be `IV`) and then 5 (`V`), implement as special cases in the start of `toRoman`
133 | 12. See if you can refactor the tests to use Vitest's [`test.each`](https://vitest.dev/api/#test-each) function
134 | 13. Continue with 6 (should be `VI`). Remember to switch "driver" after writing a new test.
135 | 14. Implement 6 first as a special case. When the test runs green, see if you can refactor to make use of the code for 5 and 1-3.
136 | 15. If you implement 6 well, you may start to see a structure in you code that you can take advantage of to continue for the whole of the exercise
137 |
138 | See [Coding Dojo's description of Roman Numerals](https://codingdojo.org/kata/RomanNumerals/) for more help and inspiration.
139 |
140 |
141 |
142 |
143 |
144 |
145 | ## Exercise 9
146 |
147 | ### Writing tests for React and Express
148 |
149 |
150 |
151 |
152 | > The purpose of exercise 9 is to teach you how to use
153 | > [`@testing-library/react`](https://testing-library.com/docs/react-testing-library/intro/) to write tests for React
154 | > and [Supertest](https://github.com/ladjs/supertest) to write tests for Express
155 |
156 |
157 |
158 | You should complete exercise 1, 2, 3 and 8 before attempting exercise 9. It's best to work in pairs on this task.
159 |
160 | ### Overview
161 |
162 | 1. Create a client directory for your React code as normal, but install `vitest` and `@testing-library/react`
163 | 2. Client: Write a test to verify that adding a task entry updates the list
164 | 3. Write a test to verify that you cannot add a task entry without a description
165 | 4. Client: Write a test to verify a snapshot of the task list page
166 | 5. Create a server directory for your Express code as normal, but install `vitest` and `supertest`
167 | 6. Write a test to verify that adding a task on the task API results in that task being on the list of tasks
168 | 7. Write a test to verify that marking a task as complete with the API updates the state of that task
169 |
170 | ### Client tests with `@testing-library/react`
171 |
172 | 1. Before you start on the client test, create a `tsconfig.js`-file in the top level directory
173 | * `npm install -D typescript`
174 | * `npx tsc --init --jsx react`
175 | 2. Create a new project or continue with the project you used for exercise 8
176 | 3. Create a `client`-subdirectory with [a React project](https://github.com/kristiania-pg6301-2024/pg6301-frontend-programming/#creating-the-frontend-project)
177 | 4. Add the testing libraries as devDependencies:
178 | * `cd client`
179 | * `npm init -y`
180 | * `npm install --save-dev vitest @testing-library/react jsdom`
181 | 5. Create a `vite.config.js`-file:
182 | ```javascript
183 | import { defineConfig } from "vite";
184 |
185 | export default defineConfig({
186 | test: {
187 | environment: "jsdom",
188 | },
189 | });
190 | ```
191 | 6. Create a `taskList.test.tsx`-file for your component tests. Here is a good first test to work on:
192 | ```tsx
193 | import { beforeEach, expect, it, vitest } from "vitest";
194 | import { cleanup, fireEvent, render } from "@testing-library/react";
195 | import React from "react";
196 |
197 | interface Props {
198 | onAddTask?: () => void;
199 | }
200 |
201 | function TaskList({ onAddTask }: Props) {
202 | return null;
203 | }
204 |
205 | beforeEach(cleanup);
206 |
207 | it("adds a new task", async () => {
208 | const handleAddTask = vitest.fn();
209 | const app = render();
210 | const description = "New Task";
211 | fireEvent.change(await app.findByLabelText("New task description:"), {
212 | value: description,
213 | });
214 | expect(handleAddTask).toBeCalledWith({ description, completed: false });
215 | });
216 | ```
217 | 7. Implement `TaskList` to get the test to pass. Refactor and move the TaskList component to a separate file.
218 | 8. Here is a possible good next (notice that this requires you to update Props for TaskList):
219 | ```tsx
220 | it("completes task", async () => {
221 | const handleCompleteTask = vitest.fn();
222 | const _id = "123";
223 | const description = "New Task";
224 | const tasks = [{ _id, description, completed: false }];
225 | const app = render(
226 | ,
227 | );
228 | fireEvent.change(await app.findByLabelText(description), { clicked: true });
229 | expect(handleCompleteTask).toBeCalledWith(_id);
230 | });
231 | ```
232 | 9. Here is a possible snapshot test to warn you if something changes:
233 | ```jsx
234 | it("renders tasks", async () => {
235 | const tasks = [
236 | { _id: "1", description: "first task", completed: false },
237 | { _id: "2", description: "second task", completed: true },
238 | ];
239 | const app = render();
240 | expect(app.baseElement).toMatchSnapshot();
241 | });
242 | ```
243 | 10. Make sure you commit your code to git as you work
244 |
245 | When you have gotten the test to run, see if you can convert the project to using TypeScript. You will need to create a
246 | `tsconfig.json`-file to convert the tests from `ts` to `js`, or else Vitest will reject `async`-functions.
247 |
248 | ### Server tests with `supertest`
249 |
250 | Continue on the same project as for the client-tests.
251 |
252 | 1. Create a `server`-subdirectory with [an Express project](https://github.com/kristiania-pg6301-2024/pg6301-frontend-programming/?tab=readme-ov-file#implement-server-side-apis-with-express)
253 | * `cd server`
254 | * `npm install -D vitest supertest @types/supertest`
255 | 2. Create a `tasksApi.test.ts` with the first test (and setup):
256 | ```typescript
257 | import { beforeAll, expect, it } from "vitest";
258 | import express from "express";
259 | import request from "supertest";
260 |
261 | function createTaskApi() {
262 | return express.Router();
263 | }
264 |
265 | const app = express();
266 | beforeAll(() => {
267 | app.use(createTaskApi());
268 | });
269 |
270 | it("adds a task to the task list", async () => {
271 | const title = "A new task created at " + new Date();
272 | const res = await request(app).post("").send({ title }).expect(200);
273 | const savedTask = res.body;
274 | expect(savedTask).toMatchObject({ title });
275 | const allTasks = (await request(app).get("").expect(200)).body;
276 | expect(allTasks).toContainEqual(savedTask);
277 | });
278 | ```
279 | 3. Make the test pass by adding `get` and `post` handlers to the Router created in `createTaskApi`.
280 | 4. When the test is passing, refactor to move `createTaskApi` to a separate file `taskApi.ts`
281 | 5. Remember to commit the code
282 | 6. Add a test that verifies that you can complete a task:
283 | ```typescript
284 | it("completes a task", async () => {
285 | const title = "Random task";
286 | const res = await request(app).post("").send({ title }).expect(200);
287 | const savedTask = res.body;
288 | expect(savedTask).toMatchObject({ title, completed: false });
289 | request(app).put(savedTask.id).send({ completed: true }).expect(201);
290 | const updatedTask = (await request(app).get(savedTask.id).expect(200)).body;
291 | expect(updatedTask).toMatchObject({ completed: true });
292 | });
293 | ```
294 | 7. Make the test pass by adding a handler for `get(":id")` and `put(":id")` to the task router
295 |
296 | ### Optional: Convert the server code to use MongoDB
297 |
298 | In order to make the server use MongoDB and the tests still work, `createTaskApi()` needs to take a MongoDB connection.
299 | In order to make the tests run with GitHub Actions, you need to use a localhost MongoDB URI and start Mongo with
300 | `docker compose` in the Workflow-file. See
301 | [workflow file in the reference code from lecture 9](https://github.com/kristiania-pg6301-2024/pg6301-frontend-programming/blob/reference/09/.github/workflows/test.yaml#L19)
302 | and the corresponding [`docker-compose.yaml`](https://github.com/kristiania-pg6301-2024/pg6301-frontend-programming/blob/reference/09/docker-compose.yaml)
303 |
304 | ### Optional: Stitching the parts together
305 |
306 | You have now created a client side component and a server side router test-first. This is a good time to add the
307 | `index.html` and `main.tsx` files to the client, implement fetching and updating logic to use `fetch` and
308 | adding the `server.ts`-file.
309 |
310 | See [Creating the frontend project](https://github.com/kristiania-pg6301-2024/pg6301-frontend-programming/?tab=readme-ov-file#creating-the-frontend-project),
311 | [Implementing server side APIs with Express](https://github.com/kristiania-pg6301-2024/pg6301-frontend-programming/?tab=readme-ov-file#implement-server-side-apis-with-express)
312 | and [Read data from an API](https://github.com/kristiania-pg6301-2024/pg6301-frontend-programming/?tab=readme-ov-file#read-data-from-an-api-in-react)
313 | in the course reference materials for details.
314 |
315 |
316 |
--------------------------------------------------------------------------------
/exercises/exercise-todo-app.md:
--------------------------------------------------------------------------------
1 | # The task application
2 |
3 | The main running exercise of this course is the classic "TODO" application. This is a very common example, and you
4 | can see lots of examples using this online. The application lets to users create tasks and mark them as complete.
5 | In addition, we will be adding details to the tasks and give access to tasks to other users.
6 |
7 | ## Exercise 1: Create a React application where you can register and add to a list of tasks
8 |
9 |
10 |
11 |
12 |
13 | > The purpose of the first week exercise is to install and verify the tools used to develop and get help for the course
14 |
15 |
16 |
17 |
18 | Your application should have the following:
19 |
20 | 1. A list of checkboxes for all created tasks
21 | 2. An input field with a submit button to add a new task
22 |
23 | You can choose to ways to create the application:
24 |
25 | ### Step 1: Install and sign up for necessary tools
26 |
27 | 1. Install [NodeJS](https://nodejs.org/en/download/package-manager) (if you don't already have it)
28 | 2. Sign up for [GitHub student developer pack](https://education.github.com/pack/join) which gives you access to
29 | important resources like IntelliJ Ultimate and Heroku for free. Make sure to use your school email address for the
30 | registration.
31 | 3. Download [IntelliJ IDEA Ultimate](https://www.jetbrains.com/idea/download/). You can use a Trial license until your
32 | GitHub student pack is registered. You can then use [the IntelliJ student page](https://www.jetbrains.com/shop/eform/students)
33 | to get a long term license
34 | 4. Sign in to https://mattermost.kristiania.no/ and find the [PG6301 channel](https://mattermost.kristiania.no/it2023/channels/pg6301---webutvikling-og-api-design) and send a message saying Hello
35 |
36 | ### Step 2: Alternative 1: Use the Vite project wizard (quick start, but lots of confusing code)
37 |
38 | Open a terminal Window, and `cd` to a directory for your course assignments. This directory should not contain
39 | spaces or special characters (`[a-zA-Z0-9._-]` are okay)
40 |
41 | 1. Run `npm init vite` - this will ask you what name you want for your project subdirectory,
42 | what framework to use (select React) and what variant to use (select JavaScript)
43 | 2. Follow the instructions from the Wizard to `cd` into the directory and run `npm install` and `npm run dev`
44 | 3. Go to http://localhost:5173/ to see your project running
45 | 4. Start IntelliJ
46 | 5. Open the project directory using File > Open
47 | 6. Navigate to `src/App.jsx` and update the code to create a TODO application
48 | 7. When you have completed your application, upload the code to GitHub
49 |
50 | ### Step 2: Alternative 2: Build the project from scratch (more steps, but no files not created by you)
51 |
52 | Open a terminal Window, and `cd` to a directory for your course assignments. This directory should not contain
53 | spaces or special characters (`[a-zA-Z0-9._-]` are okay)
54 |
55 | 1. `mkdir `: create a new directory for your project
56 | 2. `cd `: change directory to the project directory
57 | 3. `npm init -y`: creates `package.json` for your scripts and dependencies
58 | 4. `npm install --save-dev vite`: add Vite as a tool in your project
59 | 5. `npm install react react-dom`: add React as a library in your project
60 | 6. `npm pkg set scripts.dev="vite"` Add a script to run your project
61 | 7. `npm run dev`: starts up the project
62 | 8. Go to http://localhost:5173/ to see your (empty) project running
63 |
64 | You can now open the project in IntelliJ and start development
65 |
66 | 1. Start IntelliJ
67 | 2. File > Open: Open the project directory
68 | 3. Create files named `index.html` and `src/main.jsx`. See the
69 | [course material](https://github.com/kristiania-pg6301-2024/pg6301-frontend-programming/?tab=readme-ov-file#creating-the-frontend-project)
70 | to see how these files should look
71 | 4. Add the code to create a `` component with a `` component
72 | 5. When you have completed your application, upload the code to GitHub
73 |
74 | ### Step 3: Feedback and continue
75 |
76 | When you have managed to create your first React application, you should send a message saying simply
77 | "Exercise 1 complete! 🎉" at Mattermost! Feel free to include the link to your GitHub repository.
78 |
79 | If you want to explore React a bit more right away, check out the [official React tutorials](https://react.dev/learn).
80 |
81 | ### Step 4: Competition
82 |
83 | We need a logo for the course GitHub pages. Post your entry on Mattermost and vote with emojiis on other entries. Despite knowing better from experience, I will let the democratic process decide on the logo.
84 |
85 |
86 |
87 | ### Exercise solution:
88 |
89 | Check out the [reference code from lecture 1](https://github.com/kristiania-pg6301-2024/pg6301-frontend-programming/tree/reference/01)
90 |
91 | ## Exercise 2: A TODO application with updateable state
92 |
93 |
94 |
95 |
96 | >The purpose of the exercise 2 is to create functionality in a React application
97 |
98 |
99 |
100 | Starting with what you learned in exercise 1, let's transform the simple list of tasks into a more functional application.
101 | Implement the following features:
102 |
103 | * Create task
104 | * Complete task
105 | * Change task description
106 | * Show task details
107 |
108 | You should start out as we showed in the lecture, with a static component that you gradually make dynamic. Here is a possible starting point:
109 |
110 | ```jsx
111 | function TaskList() {
112 | const tasks = [
113 | { id: 1, description: "Follow the lecture", completed: true },
114 | { id: 2, description: "Read the exercise", completed: false },
115 | { id: 3, description: "Complete the exercise", completed: false },
116 | ];
117 |
118 | return
;
125 | }
126 | ```
127 |
128 |
129 | ### New task
130 |
131 | Register a new task by typing the task description in an input and pressing submit (the list of tasks should be a React `useState` with an array of objects, the current state of the input should be a `useState` with a string)
132 |
133 | ### Complete task
134 |
135 | Mark the task as completed by checking a checkbox next to the task (``)
136 | (implement by updating the task state for the checked task - this is a bit tricky)
137 |
138 |
139 |
140 | ## Exercise 3: `useEffect`, `useRef` and React Router
141 |
142 |
143 |
144 | ### Change task description
145 |
146 | Let the user update the description of an existing state by clicking a link by the task.
147 | When updating a task, use a `
165 |
166 | ### Exercise solution:
167 |
168 | Check out the [solution for exercise 2](https://github.com/kristiania-pg6301-2024/pg6301-frontend-programming/tree/exercise/02/solution)
169 |
170 | ## Exercise 4: The TODO API
171 |
172 |
173 |
174 |
175 | > The purpose of exercise 4 is to implement a server with ExpressJS for the TODO application so that your actions will be stored if you reopen the web browser
176 |
177 |
178 |
179 | Starting from what you did in exercise 2 and 3, we want to implement an ExpressJS server API for our tasks. If you have organized your frontend code like me, you
180 | will have code that looks something like this:
181 |
182 | ```jsx
183 | export function TaskApplication() {
184 | const [tasks, setTasks] = useState([]);
185 |
186 | const [editingTaskId, setEditingTaskId] = useState()
187 |
188 | function handleAddTask(task) {
189 | // ...
190 | }
191 |
192 | function handleTaskCompleted(id) {
193 | // ...
194 | }
195 |
196 | function handleChangeTask(id) {
197 | setEditingTaskId(id);
198 | }
199 |
200 | function handleCloseDialog() {
201 | setEditingTaskId(undefined);
202 | }
203 |
204 | function handleUpdateTask(id, taskDelta) {
205 | // ...
206 | }
207 |
208 |
209 | return
222 | }
223 | ```
224 |
225 | In this exercise, you should replace the loading the initial tasks, handleAddTask, handleTaskCompleted and handleUpdateTask
226 | as `fetch` calls to the ExpressJS server.
227 |
228 | In your project, create a `client` and a `server` subdirectory with separate `package.json` files.
229 |
230 | See [course notes](https://github.com/kristiania-pg6301-2024/pg6301-frontend-programming/?tab=readme-ov-file#converting-react-to-serve-from-express) for details.
231 |
232 |
233 | ### Task 3: Get the server ready to run on Heroku
234 |
235 | When you actually run the server on a cloud hosting provider like Heroku, Vite will not be running. Instead, you will run
236 | execute `vite build` when you make a change to the application and ExpressJS will serve your React code as static files.
237 |
238 | These are the high level steps, see the [course notes](https://github.com/kristiania-pg6301-2024/pg6301-frontend-programming/?tab=readme-ov-file#deploy-to-heroku) for details.
239 |
240 | 1. Create a top level `package.json` file with a `build` script that builds the client project by executing `vite build`
241 | 2. Add a top level `start` script
242 | 3. Add a static resource to the ExpressJS application in `server.js`: `app.use(express.static("../client/dist"))`
243 | 4. To make React Routes work, you also need to [add middleware to handle default requests](https://github.com/kristiania-pg6301-2024/pg6301-frontend-programming/?tab=readme-ov-file#express-middleware-for-dealing-with-browserrouter)
244 |
245 |
246 |
247 | ### Exercise solution:
248 |
249 | Check out the [solution for exercise 4](https://github.com/kristiania-pg6301-2024/pg6301-frontend-programming/tree/exercise/04/solution)
250 |
251 |
252 | ## Exercise 5: Deploy the application to Heroku
253 |
254 |
255 |
256 |
257 | > The purpose of exercise 5 is to get the code we've built to run on the Heroku cloud platform
258 |
259 |
260 |
261 | Starting from the client+server application you created in exercise 3, in exercise 4, you should create a Heroku account and deploy your application there.
262 | You should complete exercise 1, 2 and 4 before you start exercise 5, but you don't need to complete exercise 3.
263 |
264 | See [course notes](https://github.com/kristiania-pg6301-2024/pg6301-frontend-programming?tab=readme-ov-file#deploy-to-heroku) for details.
265 |
266 | ### Exercise solution:
267 |
268 | There is no exercise solution for this week's exercise
269 |
270 |
271 | ## Exercise 6: Setup verification scripts to improve the code
272 |
273 |
274 |
275 | > In exercise 6, you should add `npm test` to verify your code and run it when pushing to GitHub and Heroku
276 |
277 | Following the [lecture notes](https://github.com/kristiania-pg6301-2024/pg6301-frontend-programming#lecture-6-quality-code-prettier-jest-husky-and-github-actions),
278 | set up the following tools to verify the quality of your code:
279 |
280 | * Husky
281 | * Prettier
282 | * Typescript
283 | * GitHub Actions
284 |
285 | Add `npm test` to the `npm build` action so that Heroku is stopped from deploying a problematic version.
286 |
287 | Make sure that you create a repository on GitHub and push to it to verify the GitHub Actions.
288 |
289 |
290 |
291 | ## Exercise 7: Save data with MongoDB
292 |
293 |
294 |
295 | > In exercise 7, you should store data to MongoDB
296 |
297 | Following the [lecture notes](https://github.com/kristiania-pg6301-2024/pg6301-frontend-programming?tab=readme-ov-file#mongodb),
298 | convert the Express APIs for `GET /api/tasks` and `POST /api/tasks` to use MongoDB to store
299 | and retrieve tasks from the database.
300 |
301 | You need to sign up for [MongoDB](https://www.mongodb.com/cloud/atlas/register) for free to create a database.
302 |
303 |
304 |
--------------------------------------------------------------------------------