diff --git a/assignments/02-events-http.md b/assignments/02-events-http.md new file mode 100644 index 0000000..1e126c1 --- /dev/null +++ b/assignments/02-events-http.md @@ -0,0 +1,357 @@ +# Week 2 Assignment Event Handlers, HTTP Servers, and Express + +## Assignment Instructions +- Create an `assignment2` folder inside your `node-homework` folder if it doesn't already exist. +- Create an `assignment2` ```git branch``` before you start. +- **Testing Tool:** For testing POST requests and API endpoints, use the **Postman VS Code Extension**. Install it from the VS Code Extensions marketplace if you haven't already. This extension allows you to test localhost requests directly from VS Code without needing the desktop agent. For installation instructions, see the [Postman VS Code Extension documentation](https://learning.postman.com/docs/developer/vs-code-extension/install/). + +### Task 1: Practice With An Event Emitter and Listener +- Inside your `assignment2` folder, create a file called `events.js`. +- Create an emitter. Use an `emitter.on()` statement to listen to this emitter for the 'time' event. Whenever the listener receives the event, it should print out "Time received: " followed by the string it receives. Then, call `setInterval(callback, 5000)`. Your callback for the `setInterval` should emit a 'time' message with the current time as a string. Try it out. You use `Ctrl-C` to end the program. +- **Important:** Make sure to export your emitter (e.g. `export default emitter`) so it can be accessed by tests and other files. The project uses ESM; tests are run with Vitest. +### Task 2: Practice with the HTTP Server +Inside your `assignment2` folder, modify your `sampleHTTP.js` file. First, add the following to the top of your file: + +```js +const htmlString = ` + + + +

Clock

+ +

+ + + +`; +``` + +This, of course, is a web page. +Then change your logic so that you handle requests for the URL `'/time'`. A JSON document should be returned with an attribute `'time'` that has a value of the current time as a string. + +Once you've got that code in place, restart your server and test the new URL from your browser. Then, add logic to handle requests for `'/timePage'`. It should return the page above. +You will need to set the header content-type to be: `"text/html; charset=utf-8"`. See documentation about headers [here](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Type). + +Then restart your server and try that URL. You should see the page with a button. +Click on the button, and you should see the time. The button causes a fetch to your server. +You have now coded your first REST API. + +### Task 3: Creating your First Express Application +You need Express. It is not part of the Node base, so be sure it is installed in your `node-homework` repository: + +```bash +npm install express +``` + +Actually, your `node-homework` repository already has all the required packages installed. +If you were setting up your own project, you’d need to install them manually using `npm install`. + +In the root of `node-homework`, create a file called `app.js`, with the following code: + +```js +import express from "express"; + +const app = express(); + +app.get("/", (req, res) => { + res.send("Hello, World!"); +}); + +const port = process.env.PORT || 3000; +const server = app.listen(port, () => + console.log(`Server is listening on port ${port}...`), + ); + +export { app, server }; +``` +Start this app from your VSCode terminal with: + +```bash +node app.js +``` + +Go to your browser, and go to the URL http://localhost:3000. +Ah, ok, Hello World. It's a start. + +This file, `app.js`, is the first file for your final project. You'll keep adding on to this file and creating modules that it calls. + +**Let's explain the code** + +You call `express()` to create the app. You need a route handler for the app if it is to do anything. +The `app.get` statement tells the app about a route handler function to call when there is an HTTP GET request for `"/"`. +You tell the app to start listening for such requests. By default, it listens on `port 3000`, but if there is an environment variable set, it will use that value for the port. +The `listen()` statement might throw an error, typically because there is another process listening on the same `port`. + +Route handlers for an operation on a route are passed two or three parameters. +The `req` parameter gives the properties of the request. The `res` parameter is used to respond to the request. +The other parameter that might be passed is `next`. When `next` is passed, it contains another route handler function. If the route handler function for the route doesn't take care of the request, it can pass it on to `next()`. + +The `export { app, server }` statement exports these values so they can be used by your TDD tests. The tests are run with Vitest and use Supertest, which needs the Express `app` (and optionally the `server` for cleanup). Also, `app.listen()` is asynchronous, with a callback, but it does return the value of `server` synchronously, before the listen operation has completed. That suffices for Supertest. + +**Be Careful of the Following** + +Make sure that your route handlers respond to each request exactly once. +Stop the server with a `Ctrl-C`. Make the following change, and then restart the server: + +```js +app.get("/", (req, res) => { +// res.send("Hello, World!"); + console.log("Hello, World") +}); +``` +Then try http://localhost:3000 again. +Nothing happens until eventually the browser times out. This is bad. +Once again, stop the server, make this change, and then restart the server. + +```js +app.get("/", (req, res) => { + res.send("Hello, World!"); + res.send("Hello, World!"); +}); +``` +In this case, the browser does see a response, but in the server log, you see a bad error message. +If you see this error in future development, you'll know what caused it. + +Now try this (you have to restart the server again): + +```js +app.get("/", (req, res) => { +// res.send("Hello, World!"); + throw(new Error("something bad happened!")); +}); +``` +As you can see, a bad error appears on your browser screen, as well as in your server log. +Every Express application needs an error handler. + +An error handler in Express is like a route handler, except that it has four parameters instead of three. +They are `err`, `req`, `res`, and `next`. Add the following code after your `app.get()` block: + +```js +app.use((err, req, res, next) => { + console.log(`A server error occurred responding to a ${req.method} request for ${req.url}.`, err.name, err.message, err.stack); + if (!res.headersSent) { + res.status(500).send("A server error occurred."); + } +}); +``` +Then try the same URL again from your browser. +The user sees a terse error message this time, and the server log includes information about what caused the error, including some useful information in the req object. + +Note that the error handler checks to see if a response has already been sent. +The error might have been thrown after a response was sent, so if you try to send a response again, it will throw an error — and an error thrown by an error handler would be unfortunate. + +The error handler is configured with `app.use()`, which is what you use for middleware. +Any request: GET, POST, PATCH, and so on, are handled by this middleware, but only if an error is thrown. The default result code for any send is `200`, which means `OK`, but in this case you are setting it to `500`, which means internal server error. +If you need to set the status, you do it before you send the response. + +At this point, you can put the `app.get()` for `"/"` back to what it was. + +## Nodemon + +Nodemon saves time. It automatically restarts your app server when you make a code change. You install it with: + +```bash +npm install nodemon --save-dev +``` + +You are installing it as a development dependency. You do not want it included in any image deployed to production. + +Edit your `package.json`, so that the scripts stanza includes this line: + +```json +"scripts": { + "dev": "nodemon app.js" +} +``` + +Then run ```npm run dev``` from the command line. +Your server will start and automatically restart whenever you change your code. You can still stop it with `Ctrl-C`. + +**Staying Organized** + +You don't want all your Express code in `app.js`. That would be a mess. +There are standard ways to organize it. +The error handler is middleware. So, create a middleware folder inside `node-homework`. Within it, create a file called `error-handler.js`. Do an `npm install` of `http-status-codes`. You use the values in this component instead of numbers like `500`. +Put this code in `error-handler.js` + +```js +import { StatusCodes } from "http-status-codes"; + +const errorHandlerMiddleware = (err, req, res, next) => { + console.error( + "Internal server error: ", + err.constructor.name, + JSON.stringify(err, ["name", "message", "stack"]), + ); + + if (!res.headersSent) { + return res + .status(StatusCodes.INTERNAL_SERVER_ERROR) + .send("An internal server error occurred."); + } +}; + +export default errorHandlerMiddleware; +``` +Then, in `app.js`, take out the error handler code and substitute this: + +```js +import errorHandler from "./middleware/error-handler.js"; +app.use(errorHandler); +``` +Test the result. +In an Express application, the error handler goes after all of the routes you declare. You can now take the `throw()` statement out of your `app.get()`, and put the send of "Hello, World" back in. + +Within your route handlers, you may have expected or unexpected errors being thrown. +Suppose a user tries to register with an email address that has already been registered. Suppose you have configured the database to require unique email addresses. In this case, the database call returns an error you can recognize. You can catch it in your route handler and give the user an appropriate message. + +However, the database might give an unexpected error — for example, if it is down. In this case, you may as well let the error handler take care of things. +An unexpected error might occur outside of a try block. In this case, it is passed to the error handler automatically. + +An unexpected error might occur within a try block of your route handler. Within your catch block, you see that it is not one of the errors you expected. You can just throw the error, or better, call `next(err)` to pass it on to the error handler. + +## More Middleware + +Try this URL: http://localhost:3000/nonsense. Again, you get an error — a 404. You've seen those. You need to handle this case. + +Create a file `./middleware/not-found.js`. +You need a `req` and a `res`, but no `next` in this case. You return `StatusCodes.NOT_FOUND` and the message +```js +`You can't do a ${req.method} for ${req.url}` +``` + +Export your function (e.g. `export default notFoundMiddleware`) and add the needed `import` and `app.use()` statements in `app.js`. +Every Express application has a 404 handler like this. You put it after all the routes, but before the error handler. Then test it out. + +The middleware you have created so far is a little unusual, because in these there is no call to `next()`. Often, middleware is, as you might expect, in the middle. +A middleware function runs for some or all routes before the route handlers for those routes, but then, instead of calling `res.send()` or an equivalent, it calls `next()` to pass the work on. + +Note: There are two ways to call `next()`. +If you call `next()` with no parameters, Express calls the next route handler in the chain. Sometimes, the only one left is the not-found handler. But if you call `next(e)`, `e` should be an Error object. +In this case, the error handler is called, and the error is passed to it. + +**Exiting Cleanly** + +Your Express program opens a port. +You need to be sure that port is closed when the program exits. If there are other open connections, such as database connections, they must also be cleaned up. If not, you may find that your program becomes a zombie process, and that the port you had been listening on is still tied up. This is especially important when you are running a debugger or an automated test. You also need to catch errors the server reports with a `server.on()` statement. + +Here is some code to put at the bottom of `app.js`. Please make sure it is placed before the `export { app, server };` statement: + +```js +server.on('error', (err) => { + if (err.code === 'EADDRINUSE') { + console.error(`Port ${port} is already in use.`); + } else { + console.error('Server error:', err); + } + process.exit(1); +}); + +let isShuttingDown = false; +async function shutdown(code = 0) { + if (isShuttingDown) return; + isShuttingDown = true; + console.log('Shutting down gracefully...'); + try { + await new Promise(resolve => server.close(resolve)); + console.log('HTTP server closed.'); + // If you have DB connections, close them here + } catch (err) { + console.error('Error during shutdown:', err); + code = 1; + } finally { + console.log('Exiting process...'); + process.exit(code); + } +} + +process.on('SIGINT', () => shutdown(0)); // ctrl+c +process.on('SIGTERM', () => shutdown(0)); // e.g. `docker stop` +process.on('uncaughtException', (err) => { + console.error('Uncaught exception:', err); + shutdown(1); +}); +process.on('unhandledRejection', (reason) => { + console.error('Unhandled rejection:', reason); + shutdown(1); +}); +``` + +### Task 4: Add a Post Route Handler +Modify your `app.js`. +Add an `app.post()` for `"/testpost"`. +Send something back. Then test this new route handler using the Postman VS Code Extension. You do not need to put anything in the body. + +### Task 5: Add Logging Middleware +Modify your Express `app.js`. +Add an `app.use()` statement, above your other routes. +You can declare this middleware function inline, in `app.js`. + +The middleware function you add should do a `console.log()` of the `req.method`, the `req.path`, and the `req.query`. +Don't forget to call `next()`, as this is middleware. + +Then start the server and try sending various requests to your server from your browser, to see the log messages. +You could also try POST requests using Postman. The `req.query` object gives the query parameters. +You can add them to your request from your browser or from Postman by putting something like `?height=7&color=brown` at the end of the URL you send from your browser or Postman. +This middleware is useful — it could help with debugging. We haven't explained how to get `req.body`, but eventually you could get that as well. + +**Run The Tests** + - After completing the tasks, run the tests using: + ```bash + npm run tdd assignment2 + ``` + - Make sure all tests pass before submitting your work. + +## Video Submission + +Record a short video (3–5 minutes) on YouTube, Loom, or a similar platform. +Share the link in your submission form. + +### Video Content: +**Answer 3 questions from Lesson 2:** + +1. **How do Event Emitters and Listeners work in Node.js?** + - Explain the EventEmitter class and its purpose + - Discuss the synchronous nature of event emission + +2. **What are the key differences between Node's HTTP module and Express.js?** + - Explain what Express provides that makes development easier + - Discuss middleware and routing concepts + +3. **How do you handle different HTTP methods and routes in a web server?** + - Explain HTTP methods (GET, POST, PUT, PATCH, DELETE) + - Show how to parse request bodies and headers + - Demonstrate route handling and response formatting + - Discuss error handling and status codes + +**Video Requirements**: +- Keep it concise (3-5 minutes) +- Use screen sharing to show code examples +- Speak clearly and explain concepts thoroughly +- Include the video link in your assignment submission + +📌 Follow these steps to submit your work: + +1️⃣ Add, Commit, and Push Your Changes +Within your node-homework folder, do a `git add` and a `git commit` for the files you have created, so that they are added to the `assignment2` branch. +Push that branch to GitHub. + +2️⃣ Create a Pull Request +Log on to your GitHub account. +Open your `node-homework` repository. +Select your `assignment2` branch. It should be one or several commits ahead of your main branch. +Create a pull request. + +3️⃣ Submit Your GitHub Link +Your browser now has the link to your pull request. +Copy that link to be included in your homework submission form. + +4️⃣ **Don't forget to include your video link in the submission form!** \ No newline at end of file diff --git a/lessons/02-events-http.md b/lessons/02-events-http.md new file mode 100644 index 0000000..dc485cf --- /dev/null +++ b/lessons/02-events-http.md @@ -0,0 +1,341 @@ +# **Lesson 2 — Events, HTTP Serving, and Express** + +## **Lesson Overview** + +**Learning objective**: Students will gain familiarity with event emitters and listeners. Students will use the Node http package to create an HTTP server that handles a few routes and responds with JSON. Students will create a POST route in the http server and test it using the Postman VS Code Extension. Students will learn the elements of Express and will create a basic Express server. + +**Topics**: + +1. Event Emitters and Listeners. +2. A Simple HTTP Server +3. Testing with Postman +4. Introducing Express + +## **Understanding the Layers: HTTP vs Node vs Express** + +Before diving into the code, it's important to understand the distinction between these three concepts: + +### **HTTP (Protocol Layer)** +- **What it is**: A standardized protocol for how clients and servers communicate over the web +- **What it defines**: Request/response format, methods (GET, POST, etc.), status codes, headers +- **Where it lives**: In the network - it's the language that browsers and servers speak to each other + +### **Node.js HTTP Module (Low-level Implementation)** +- **What it is**: Node's built-in way to create HTTP servers using the HTTP protocol +- **What it provides**: Raw access to HTTP requests/responses, manual parsing, basic server creation +- **Level of abstraction**: Low - you handle everything manually (parsing bodies, setting headers, routing) + +### **Express.js (High-level Framework)** +- **What it is**: A web framework built on top of Node.js that simplifies HTTP server creation +- **What it provides**: Automatic parsing, middleware system, routing, error handling +- **Level of abstraction**: High - Express handles the HTTP complexity so you can focus on business logic + +**Think of it this way**: HTTP is the language, Node HTTP is the manual translation, and Express is the automatic translator that makes everything easier. + +## **2.1 Event Emitters and Listeners** + +Event emitters allow communication between different parts of an application. Once an event emitter has been created, listeners can sign up to get called whenever the emitter emits an event. An event has a name, and may also pass various arguments to the listeners. Create a file called events-intro.js in your node-homework/assignment2 folder, and put in the following code: + +```js +import EventEmitter from "events"; + +const emitter = new EventEmitter(); + +emitter.on("tell", (message) => { + // this registers a listener + console.log("listener 1 got a tell message:", message); +}); + +emitter.on("tell", (message) => { + // listener 2. You don't want too many in the chain + console.log("listener 2 got a tell message:", message); +}); + +emitter.on("error", (error) => { + // a listener for errors. It's a good idea to have one per emitter + console.log("The emitter reported an error.", error.message); +}); + +emitter.emit("tell", "Hi there!"); +emitter.emit("tell", "second message"); +emitter.emit("tell", "all done"); +``` + +Try this program out. We only have "tell" and "error" as event names in this case, but typically there would be more named events. The listeners for a given event are called in the order they register, and the emitting of events is synchronous, although it can be made asynchronous. + +This may seem pretty simple, and it is -- but it enables you to write programs where one function communicates with many others, depending on the conditions. The mainline code could have logic that says, if X happens, notify a, b, and c, but if Y happens, notify c and d. This gives plugpoints where developers can add modules to listen for events. This simple model is exploited extensively in the Node `http` package. + +Another package you should know about in Node is the `net` package. This allows you to create, for example, server processes for which the protocol is not HTTP. Mail servers, Domain Name Servers, whatever you like. We won't do that in this class. + +## **2.2 A Simple HTTP Server** + +The HTTP package, which is built into the Node base, allows you to create a server process that listens on a port. Each time an HTTP request is received, the http server calls the callback you specify, passing a req object, which contains information from the request, and a res object, which gives you a way to send a response to the request. Here is a sample. Create a file in your `node-homework/assignment2` folder called `sampleHTTP.js` with the following content. Then start the program in Node. + +```js +import http from "http"; + +const server = http.createServer({ keepAliveTimeout: 60000 }, (req, res) => { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + data: "Hello World!", + }), + ); +}); + +server.listen(8000); +``` + +You can access the server in your browser at `http://localhost:8000`. However, it doesn't do too much, in that, no matter what request you send to the server, all you get back is "Hello World". The callback you provide for `(req, res)` is called for every incoming request. That callback, in this case, calls two methods of the res object: `res.writeHead()`, which puts the HTTP result code and a header into the response, and `res.end()`, which sends the actual data. You note that your Node program keeps running. In Node, if there is an open network operation, or a promise being waited on, or pending file I/O, the Node process keeps running. You stop it with Ctrl-C. + +When the server gets an inbound HTTP request, it parses the basic information describing the request, including, in particular, the method, the url, the headers, and the cookies, and then issues the callback. All of that information is in the req object -- but not the body of the request, if any body is present. The data in the body is not available until you do another step. Let's add some logic: + +```js +import http from "http"; + +const server = http.createServer({ keepAliveTimeout: 60000 }, (req, res) => { + if ( + req.method === "POST" && + req.url === "/" && + req.headers["content-type"] === "application/json" + ) { + let body = ""; + req.on("data", (chunk) => (body += chunk)); // this is how you assemble the body. + req.on("end", () => { + // this event is emitted when the body is completely assembled. If there isn't a body, it is emitted when the request arrives. + const parsedBody = JSON.parse(body); + res.writeHead(200, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + weReceived: parsedBody, + }), + ); + }); + } else if (req.method != "GET") { + res.writeHead(404, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + message: "That route is not available.", + }), + ); + } else if (req.url === "/secret") { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + message: "The secret word is 'Swordfish'.", + }), + ); + } else { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + pathEntered: req.url, + }), + ); + } +}); + +server.listen(8000); +``` + +The idea, which we'll pursue further when we get to Express, is that we have a look at the request, and depending on what is in it, we send the appropriate response. Here, the logic checks the method, which is one of GET, POST, PATCH, PUT, or DELETE, and the url. In this case, the HTTP server provides not full URL but the URL path. Depending on the values of the method and the path, different responses are returned. + +If the server is running, stop it with a Ctrl-C, put in the code above, and restart it. You can then try `http://localhost:8000/testPath` and `http://localhost:8000/secret` from your browser. + +## **2.3 Testing with Postman** + +But, you also need to test the POST request. For that you use the **Postman VS Code Extension**. Browsers only send POST requests when a form is submitted. + +**Note:** Install the Postman VS Code Extension from the VS Code Extensions marketplace. This extension allows you to test localhost requests directly from VS Code without needing the desktop agent or web version. For installation instructions, see the [Postman VS Code Extension documentation](https://learning.postman.com/docs/developer/vs-code-extension/install/). + +Open the Postman VS Code Extension, and click on the New button. Select New Http Request. You will see a pulldown that defaults to "GET". Switch it to "POST". Then, where it says "Enter Request URL", put in `http://localhost:8000`. Then click on the "Body" tab, and select "raw". Then, in the pulldown that defaults to "Text", choose "JSON". Then paste the following into the body: + +```JSON +{ + "firstAttribute": "value1", + "secondAttribute": 42 +} +``` + +Then, click on the send button, and see what you get back. You get a JSON response in this case. If you change the URL to `http://localhost:8000/not-here`, you'll see you get back a 404. + +This is one perfectly valid way of creating a web application back end. Your server could respond with any kind of data, including HTML, and could handle a variety of incoming requests. As no doubt you have noticed, this is still pretty low level. You have to add logic for each URL path and method. You have to read the body. You have to parse the body. You have to handle errors if any. For example, the code above will crash the server if the incoming JSON is invalid, because the parse will throw an error. Fortunately, the Express package makes your development task much easier. + +## **2.4 Introducing Express** + +Express is not part of the Node base. You need to do an `npm install express` while in your node-homework folder. Express may already be part of your node-homework repository, but install it to be sure. We are going to use Express to create a back end, a series of REST APIs. Express can be used for other purposes, such as static file serving, or for server side rendered pages. Server side rendering provides dynamic HTML using a templating language such as EJS or Pug. We won't be doing that, and there are other more modern frameworks for server side rendered pages, such as JSX with NextJS. + +**Key Difference from Node HTTP**: While the Node HTTP module gives you raw access to HTTP requests and requires you to manually parse bodies, set headers, and handle routing, Express automatically handles these low-level details. Express builds on top of Node's HTTP capabilities to provide a higher-level, more developer-friendly interface. Think of it as the difference between building a house from scratch (Node HTTP) versus using pre-made components (Express). + +In the previous section on REST and HTTP, you learned about the components of an HTTP request: a method (such as GET, POST, PUT, PATCH, or DELETE), a path, query parameters, headers, sometimes a body, and cookies. In Express, you have the following elements: + +1. An app, as created by a call to Express. + +2. A collection of route handlers. A request for a particular HTTP method and path are sent to a route handler. For example, a POST for /notices would have a route handler that handles this route. A route handler is a function with the parameters req, res, and sometimes next. The req parameter is a structure with comprehensive information about the request. The res parameter is the way that the route handler sends the response. The next parameter is needed in case the route handler needs to throw an error to an error handler. + +3. Middleware. Middleware functions do some initial processing on the request. Sometimes a middleware function checks to see if the request should be sent on to the next piece of middleware in the chain or the route handler. If not, the middleware itself returns a response to the request. In other cases, a middleware function may add additional information to the req object, and may also set headers, including sometimes set-cookie headers, in the res object. Then it calls next(), which might call another middleware function, or might call a route handler. Middleware functions typically have three parameters, req, res, and next. A standard piece of middleware you'll create is the not-found handler, which is invoked when no route handler could be found for the method and path. + +4. An error handler. This is at the end of the chain, in case an error occurs. There is only one error handler, and it takes four parameters, err, req, res, and next, which is how Express knows it is an error handler. + +5. A server. This is created as a result of an `app.listen()` on a port. + +The mainline code in the app does the following: + +1. It creates the app. + +2. It specifies a chain of middleware functions and route handlers to be called, each with filter conditions based on the method and path of the request. These conditions determine each should be called. Middleware functions are configured in the chain via an app.use() statement. Route handlers are configured in the chain via app.get(), app.post(), and similar statements. + +**Order matters in this configuration.** The first app.use(), app.get(), or other such statement that is matched determines what function is called. If that is a middleware function, it will often do a next, and then the next middleware function or route handler in the chain that matches the HTTP method and path is called. While middleware often does a next(), route handlers only do a next(err), in cases when an error should be passed to the error handler. + +3. It tells the app to listen on a port. + +## **An example Express Server** + +Here is an example of what an Express app.js might look like. This is just an example — you don't need to put this code in a file. + +```js +import express from "express"; + +const app = express(); + +// the following statements configure the chain of middleware and route handlers. Nothing happens with them until a request is received. + +app.use((req, res, next) => { + // this is called for every request received. All methods, all paths + req.additional = { this: 1, that: "two" }; + const content = req.get("content-type"); + if (req.method == "POST" && content != "application/json") { + next(new Error("A bad content type was received")); // this invokes the error handler + } else { + next(); // as OK data was received, the request is passed on to it. + } +}); + +app.get("/info", (req, res) => { + // this is only called for get requests for the specific path + res.send("We got good stuff here!"); +}); + +app.use("/api", (req, res, next) => { + // this is called for all methods, but only if the path begins with /api + // and only if the request got past that first middleware. + // ... + next(); // have to either send a response or pass to next -- else the user waits until timeout. +}); + +app.use((req, res) => { + // this is the not found handler. Nothing took care of the request, so we send the caller the bad news. You always need one of these. + res.status(404).send("That route is not present."); +}); + +app.use((err, req, res, next) => { // The error handler. You always need one. + console.log(err.constructor.name, err.message, err.stack); + res.status(500).send("internal server error"); +}); + +const port = process.env.PORT || 3000; + +const server = app.listen(port, ()=>{ + console.log("server listening on port ", port); +}); +``` + +Of course, for a real app, the route handlers and middleware functions would not be declared inline. Suppose one set of route handlers deals with customers, via GET/POST/PATCH/PUT/DELETE requests on all paths that start with `/customers`, and suppose you have another set for `/orders`. Typically you would have a `./controllers` folder, with a `customerController.js` for all the route handlers for customers and an `orderController.js` for all the route handlers for orders. Typically also, you would have a `./routes` folder, for modules that create express routers. An express router is a way of associating a collection of routes with a collection of route handlers. You'd also have a middleware folder. In the mainline code, you might have statements like: + +```js +import customerRouter from "./routes/customer.js"; +import customerMiddleware from "./middleware/customerMiddleware.js"; +app.use("/customers", customerMiddleware, customerRouter); +``` + +The customerMiddleware function might check if the customer is logged in. If not, it could send a response with a 401 status code, possibly with an error message in the body. If a customer is logged in, the middleware function might add additional information to the req object. It would call next, so that processing would pass to the customerRouter. + +For each request sent to the server, there must be exactly one response. If no response is sent, a user might be waiting at the browser for a timeout. If several responses are sent for one request, Express reports an error instead of sending the second one. + +## **What do Request Handlers Do?** + +Request handlers may retrieve data and send it back to the caller. Or, they may store, modify, or delete data, and report the success or failure to the caller. Or, they may manage the session state of the caller, as would happen, for example, with a logon. The data accessed by request handlers may be in a database, or it may be accessed via some network request. When sending the response, a request handler might send plain text, HTML, or JSON, or any number of other content types. A request handler must either send a response or call the error handler to send a response. Otherwise the request from the caller will wait until timeout. + +Route handlers and middleware frequently do asynchronous operations, often for database access. While the async request is being processed, other requests may come to the server, and they are dispatched as usual. Route handlers and middleware may be declared as async, so that the async/await style of programming can be used. These functions don't return a value of interest. + +## **Middleware Functions, Route Handlers, and Error Handling** + +Let's sum up common characteristics of middleware functions and response handlers. + +1. Middleware functions typically take three parameters — `req`, `res`, and `next` — which allow them to process the request, modify the response, or pass control to the next function. They may be declared as async functions. + +2. Once they are called, these functions do processing based on the information in the req object: method, path, path parameters, query parameters, headers, cookies, the body. Every request has a method and path, but the other request attributes may or may not be present. + +3. These functions must do one of the following, or the request times out: + +- Send a response. +- Call next(). +- Throw an error. + +Even route handlers sometimes call next(). In these cases they call `next(error)` to pass the error to the error handler. Middleware functions often call next() without parameters, to call the next middleware in the chain or the route handler for the request, but they might call `next(error)` in some cases. + +4. If `next(error)` is called or an error is thrown, the error handler is called and passed the error. In Express 5, this happens even if the error occurs while an async middleware function or route handler is waiting on an asynchronous operation. **However, please note:** Middleware functions and route handlers sometimes call functions that have callbacks. You must **never** throw an error from within a callback, because that would crash the server. Instead, call next(error), which properly passes the error to Express’s error handling system so it can be handled gracefully without affecting other users. + + +## **Parsing the Body of a JSON Request** + +One very common piece of middleware is the following: + +```js +app.use(express.json()) +``` + +This middleware parses the body of a request that has content-type "application/json". The resulting object is stored in req.body. There are other body parsers to be used in other circumstances, for example to catch data that is posted from an HTML form. + +### **The req and res Objects** + +You can access the following elements of the req: + +req.method +req.path +req.params HTML path parameters, if any. When you configure a route with a route handler, you can tell Express where these are in the URL. +req.query query parameters of the request, if any +req.body The body of the request, if any +req.host The host that this Express app is running on + +There are many more. + +The `req.get(headerName)` function returns the value of a header associated with the request, if that header is present. +`req.cookies[cookiename]` returns the cookie of that name associated with request, if one is present. + +The `res` object has the following methods: + +- `res.status()` This sets the HTTP status code +- `res.cookie()` Causes a Set-Cookie header to be attached to the response. +- `res.setHeader()` Sets a header in the response +- `res.json()` When passed a JavaScript object, this method converts the object to JSON and sends it back to the originator of the request +- `res.send()` This sends plain text data, or perhaps HTML. + +### **Check for Understanding** + +1. What are the parameters always passed to a route handler? What are they for? + +2. What must a route handler always do? + +3. How does a middleware function differ from a route handler? + +4. If you do an await in a route handler, who has to wait? + +5. You add a middleware function or router to the chain with an app.use() statement. You add a route handler to the chain with an app.get() or similar statement. Each of these has filter conditions. How do the filter conditions differ? + +### **Answers** + +1. A route handler always gets the req and res objects. The req object contains information from the request, perhaps with additional attributes added by middleware functions. The res object has methods including res.send() and res.json() that enable the route handler to respond to the request. + +2. A route handler must either respond to the request with res.send() or res.json(), or call the error handler with next(error), or throw the error. + +3. A middleware function may or may not respond to the request. Instead it may call next() to pass control to the next handler or middleware function in the chain. It must either respond to the request or call next, or perhaps throw an error. + +4. If you do an await in a route handler, the caller for the request has to wait. You might be waiting, for example, on a response from the database. You need the database response before you can send the HTTP response to the caller. On the other hand, other callers don't have to wait, unless they make a request that ends up at an await statement. + +5. An app.use() statement has an optional parameter, which is the path prefix. You might have a statement like app.use("/api", middlewareFunction), and then middlewareFunction would be called for every request with a path starting with "/api", no matter if it is a GET, a POST, or whatever. If the path prefix is omitted, the middleware function is called for every request. An app.get() statement, on the other hand, calls the corresponding route handler only if the path matches exactly and the method is a GET. In either case, you can pass additional middleware functions as parameters to be called in order, like: + +```js +app.use("/api", middleware1, middleware2, middleware3); +app.get("/info", middleware1, infoHandler); +``` \ No newline at end of file diff --git a/mentor-guidebook/sample-answers/assignment2/app.js b/mentor-guidebook/sample-answers/assignment2/app.js new file mode 100644 index 0000000..90eddb7 --- /dev/null +++ b/mentor-guidebook/sample-answers/assignment2/app.js @@ -0,0 +1,69 @@ +import express from "express"; +import errorHandler from "../middleware/error-handler.js"; +import notFound from "../middleware/not-found.js"; + +const app = express(); + +app.use((req, res, next) => { + console.log("Method:", req.method); + console.log("Path:", req.path); + console.log("Query:", req.query); + next(); +}) + +app.get("/", (req, res) => { + res.send("Hello, World!"); +}); + +app.post("/testpost", (req, res) => { + res.json({message: "Everything Worked."}); +}) + +app.use(notFound); +app.use(errorHandler); + + +const port = process.env.PORT || 3000; +const server = app.listen(port, () => + console.log(`Server is listening on port ${port}...`), +); + +server.on('error', (err) => { + if (err.code === 'EADDRINUSE') { + console.error(`Port ${port} is already in use.`); + } else { + console.error('Server error:', err); + } + process.exit(1); +}); + +let isShuttingDown = false; +async function shutdown(code = 0) { + if (isShuttingDown) return; + isShuttingDown = true; + console.log('Shutting down gracefully...'); + try { + await new Promise(resolve => server.close(resolve)); + console.log('HTTP server closed.'); + // If you have DB connections, close them here + } catch (err) { + console.error('Error during shutdown:', err); + code = 1; + } finally { + console.log('Exiting process...'); + process.exit(code); + } +} + +process.on('SIGINT', () => shutdown(0)); // ctrl+c +process.on('SIGTERM', () => shutdown(0)); // e.g. `docker stop` +process.on('uncaughtException', (err) => { + console.error('Uncaught exception:', err); + shutdown(1); +}); +process.on('unhandledRejection', (reason) => { + console.error('Unhandled rejection:', reason); + shutdown(1); +}); + +export { server, app }; \ No newline at end of file diff --git a/mentor-guidebook/sample-answers/assignment2/assignment2/events-intro.js b/mentor-guidebook/sample-answers/assignment2/assignment2/events-intro.js new file mode 100644 index 0000000..a4c2676 --- /dev/null +++ b/mentor-guidebook/sample-answers/assignment2/assignment2/events-intro.js @@ -0,0 +1,22 @@ +import EventEmitter from "events"; + +const emitter = new EventEmitter(); + +emitter.on("tell", (message) => { + // this registers a listener + console.log("listener 1 got a tell message:", message); +}); + +emitter.on("tell", (message) => { + // listener 2. You don't want too many in the chain + console.log("listener 2 got a tell message:", message); +}); + +emitter.on("error", (error) => { + // a listener for errors. It's a good idea to have one per emitter + console.log("The emitter reported an error.", error.message); +}); + +emitter.emit("tell", "Hi there!"); +emitter.emit("tell", "second message"); +emitter.emit("tell", "all done"); \ No newline at end of file diff --git a/mentor-guidebook/sample-answers/assignment2/assignment2/events.js b/mentor-guidebook/sample-answers/assignment2/assignment2/events.js new file mode 100644 index 0000000..1cd2d16 --- /dev/null +++ b/mentor-guidebook/sample-answers/assignment2/assignment2/events.js @@ -0,0 +1,17 @@ +import EventEmitter from "events"; + +const emitter = new EventEmitter(); + +emitter.on("time", (message) => { + console.log("Time received: ", message); +}); + +const isMain = process.argv[1] && process.argv[1].endsWith("events.js"); +if (isMain) { + setInterval(() => { + const currentTime = new Date().toString(); + emitter.emit("time", currentTime); + }, 5000); +} + +export default emitter; \ No newline at end of file diff --git a/mentor-guidebook/sample-answers/assignment2/assignment2/sampleHTTP.js b/mentor-guidebook/sample-answers/assignment2/assignment2/sampleHTTP.js new file mode 100644 index 0000000..1474688 --- /dev/null +++ b/mentor-guidebook/sample-answers/assignment2/assignment2/sampleHTTP.js @@ -0,0 +1,74 @@ +import http from "http"; + +const htmlString = ` + + + +

Clock

+ +

+ + + +`; + +const server = http.createServer({ keepAliveTimeout: 60000 }, (req, res) => { + if ( + req.method === "POST" && + req.url === "/" && + req.headers["content-type"] === "application/json" + ) { + let body = ""; + req.on("data", (chunk) => (body += chunk)); + req.on("end", () => { + const parsedBody = JSON.parse(body); + res.writeHead(200, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + weReceived: parsedBody, + }), + ); + }); + } else if (req.method != "GET") { + res.writeHead(404, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + message: "That route is not available.", + }), + ); + } else if (req.url === "/secret") { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + message: "The secret word is 'Swordfish'.", + }), + ); + } else if (req.url === "/time") { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + time: new Date().toString(), + }), + ); + } else if (req.url === "/timePage") { + res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }); + res.end(htmlString); + } else { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + pathEntered: req.url, + }), + ); + } +}); + +server.listen(8000); diff --git a/mentor-guidebook/sample-answers/assignment2/middleware/error-handler.js b/mentor-guidebook/sample-answers/assignment2/middleware/error-handler.js new file mode 100644 index 0000000..0ce2391 --- /dev/null +++ b/mentor-guidebook/sample-answers/assignment2/middleware/error-handler.js @@ -0,0 +1,17 @@ +import { StatusCodes } from "http-status-codes"; + +const errorHandlerMiddleware = (err, req, res, next) => { + console.error( + "Internal server error: ", + err.constructor.name, + JSON.stringify(err, ["name", "message", "stack"]), + ); + + if (!res.headersSent) { + return res + .status(StatusCodes.INTERNAL_SERVER_ERROR) + .send("An internal server error occurred."); + } +}; + +export default errorHandlerMiddleware; \ No newline at end of file diff --git a/mentor-guidebook/sample-answers/assignment2/middleware/not-found.js b/mentor-guidebook/sample-answers/assignment2/middleware/not-found.js new file mode 100644 index 0000000..24f09a8 --- /dev/null +++ b/mentor-guidebook/sample-answers/assignment2/middleware/not-found.js @@ -0,0 +1,9 @@ +import { StatusCodes } from "http-status-codes"; + +const notFoundMiddleware = (req, res) => { + return res + .status(StatusCodes.NOT_FOUND) + .send(`You can't do a ${req.method} for ${req.url}`); +}; + +export default notFoundMiddleware; \ No newline at end of file