├── .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 |

Welcome to my application

508 |
509 | 510 |
511 |
You have clicked {counter} times
512 | ; 513 | } 514 | ``` 515 | 516 | #### Optional: Enable hot refresh with Vite 517 | 518 | `npm install -D @vitejs/plugin-react` and add the following `vite.config.js` in your client project: 519 | 520 | ```js 521 | import {defineConfig} from "vite"; 522 | import reactVite from "@vitejs/plugin-react"; 523 | 524 | export default defineConfig({ 525 | plugins: [reactVite()] 526 | }); 527 | ``` 528 | 529 |
530 | 531 | #### React Router 532 | 533 |
534 | 535 | ```jsx 536 | export function MoviesApplication() { 537 | return 538 | 539 | } /> 540 | } /> 541 | 542 | ; 543 | } 544 | 545 | function Movies() { 546 | return 547 | } /> 548 | } /> 549 | 550 | } 551 | 552 | function FrontPage() { 553 | return
554 |

Front Page

555 |
    556 |
  • List existing movies
  • 557 |
  • Add new movie
  • 558 |
559 |
; 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 |
732 |

Add Movie

733 |
734 | Title: 735 |
736 | setTitle(e.target.value)} /> 737 |
738 |
739 | 740 |
741 |
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
119 |

Tasks

120 | {tasks.map(({id, description, completed}) => )} 124 |
; 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 ``. 148 | 149 | 1. `useState` with a `dialogOpen` state that reflects the state of the dialog 150 | 2. `useRef` to refer to the `` element and `useEffect` to `showModal()` when `dialogOpen` updates 151 | 3. Submitting the form in the dialog should close the dialog 152 | 153 | ### Close the dialog correctly 154 | 155 | If you press Escape in the dialog for updating task title, you may be unable to click the dialog open again. 156 | This is because the state of `dialogOpen` has drifted away from the state of the HTML elements. Add a close listener 157 | to the dialog (using the `useRef` reference) to update `dialogOpen` state when the user closes the dialog. 158 | 159 | ### Show task details with a router 160 | 161 | Add `react-router-dom` as a dependency. Clicking on a task should take you to another route that focuses on the task. 162 | You can choose whether this page just displays the task description or if you want to add more info. 163 | 164 |
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
210 | 215 | 216 | t.id === editingTaskId)} 218 | onUpdateTask={handleUpdateTask} 219 | onClose={handleCloseDialog} 220 | /> 221 |
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 | --------------------------------------------------------------------------------