diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..c35aab4a --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +package-lock.json +.env +.DS_Store +*.log +dist/ +build/ +.vscode/ \ No newline at end of file diff --git a/middleware-exercise/README.md b/middleware-exercise/README.md new file mode 100644 index 00000000..385bb0d2 --- /dev/null +++ b/middleware-exercise/README.md @@ -0,0 +1,65 @@ +# Middleware Exercise: Custom vs. Off-the-Shelf + +This project demonstrates the implementation of Middleware in Express.js. It explores two ways of handling request data: manually parsing raw data streams and using built-in Express tools. + +## ๐Ÿš€ How to Run + +1. **Install dependencies:** + ```bash + npm install + ``` + +2. **Run the Custom Middleware version:** + ```bash + node "two custom-written middlewares.js" + ``` + +3. **Run the Off-the-Shelf version:** + ```bash + node off-the-shelf-middleware.js + ``` + +*Note: The server defaults to port `3000`. To use a specific port, use: `PORT=4000 node [filename].js`* + +## ๐Ÿ›  Key Concepts Learned + +### 1. The Middleware Pattern +Middleware functions sit between the request and the final route handler. In this project: +- **Username Middleware:** Extracts identity from headers. +- **Validation Middleware:** Ensures the incoming data matches the expected format before the route logic runs. + +### 2. Manual Parsing vs. `express.json()` +- **Custom Parsing:** Uses `req.on("data")` and `req.on("end")` to catch raw bytes, translate them via `String.fromCharCode`, and parse them. +- **Built-in Parsing:** Uses `express.json()`, which requires the client to send the `Content-Type: application/json` header. + +### 3. Environment Variables & Safety +- **Port Fallback:** Used `process.env.PORT` with a fallback to `3000`. +- **Parsing Numbers:** Used `parseInt(..., 10)` to ensure the port string from the environment is converted to a base-10 integer. +- **ES Modules:** Set `"type": "module"` in `package.json` to allow the use of modern `import/export` syntax. + +### 4. Process Management +Learned how to identify and terminate "zombie" processes hanging on a port using: +```bash +sudo lsof -iTCP:3000 -sTCP:LISTEN -Pn +sudo kill -9 +``` + +## ๐Ÿงช Testing with `curl` + +To test the application, use the following commands in your terminal: + +**Valid Request (with Username):** +```bash +curl -X POST -H "Content-Type: application/json" -H "X-Username: Ahmed" -d '["Bees", "Birds"]' http://localhost:3000/ +``` + +**Valid Request (Guest):** +```bash +curl -X POST -H "Content-Type: application/json" -d '["Lizards"]' http://localhost:3000/ +``` + +**Invalid Data (should return 400):** +```bash +curl -X POST -H "Content-Type: application/json" -d 'notjson' http://localhost:3000/ +``` + diff --git a/middleware-exercise/off-the-shelf-middleware.js b/middleware-exercise/off-the-shelf-middleware.js new file mode 100644 index 00000000..d9544b5d --- /dev/null +++ b/middleware-exercise/off-the-shelf-middleware.js @@ -0,0 +1,60 @@ +import express from "express"; + +const app = express(); + +const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000; + +const usernameMiddleware = (req, res, next) => { + const headerValue = req.get("X-username"); + + if (headerValue) { + req.username = headerValue; + } else { + req.username = null; + } + + next(); +}; + +app.use(express.json()); + +const validateArray = (req, res, next) => { + if ( + req.body && + Array.isArray(req.body) && + req.body.every((item) => typeof item === "string") + ) { + next(); + } else { + res.status(400).send("Error message"); + } +}; + +// Hey Express, whenever a POST request arrives at '/', please call this person first (usernameMiddleware), then this person (arrayMiddleware), then finally do the route logic. +app.post("/", usernameMiddleware, validateArray, (req, res) => { + let authPart; + if (req.username) { + authPart = `You are authenticated as ${req.username}`; + } else { + authPart = "You are not authenticated"; + } + const MessageCount = req.body.length; + + const messageJoined = req.body.join(","); + + const word = MessageCount === 1 ? "subject" : "subjects"; + + if (MessageCount > 0) { + res.send( + `${authPart}\n\nYou have requested information about ${MessageCount} ${word}: ${messageJoined}.`, + ); + } else { + res.send( + `${authPart}\n\nYou have requested information about ${MessageCount} ${word}.`, + ); + } +}); + +app.listen(PORT, () => { + console.log("Type your message here"); +}); diff --git a/middleware-exercise/package.json b/middleware-exercise/package.json new file mode 100644 index 00000000..8a23acfd --- /dev/null +++ b/middleware-exercise/package.json @@ -0,0 +1,16 @@ +{ + "name": "middleware-exercise", + "version": "1.0.0", + "description": "", + "main": "index.js", + "type": "module", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "express": "^5.2.1" + } +} diff --git a/middleware-exercise/two-custom-written-middlewares.js b/middleware-exercise/two-custom-written-middlewares.js new file mode 100644 index 00000000..f75c3267 --- /dev/null +++ b/middleware-exercise/two-custom-written-middlewares.js @@ -0,0 +1,82 @@ +import express from "express"; + +const app = express(); + +const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000; + +const usernameMiddleware = (req, res, next) => { + const headerValue = req.get("X-username"); + + if (headerValue) { + req.username = headerValue; + } else { + req.username = null; + } + + next(); +}; + +const arrayMiddleware = (req, res, next) => { + const bodyBytes = []; + + // Every time a piece of data arrives, we put it in our list + req.on("data", (chunk) => { + bodyBytes.push(...chunk); + }); + + // When the whole message has arrived, we process it + req.on("end", () => { + const bodyString = String.fromCharCode(...bodyBytes); + + let bodyObject; + try { + bodyObject = JSON.parse(bodyString); + } catch (error) { + res.status(400).send("Invalid JSON"); + return; + } + + if ( + Array.isArray(bodyObject) && + bodyObject.every((item) => typeof item === "string") + ) { + req.body = bodyObject; + next(); + } else { + res + .status(400) + .send( + "Invalid request body. Expected a JSON array containing only strings.", + ); + } + }); +}; + +// Hey Express, whenever a POST request arrives at '/', please call this person first (usernameMiddleware), then this person (arrayMiddleware), then finally do the route logic. +app.post("/", usernameMiddleware, arrayMiddleware, (req, res) => { + let authPart; + if (req.username) { + authPart = `You are authenticated as ${req.username}`; + } else { + authPart = "You are not authenticated"; + } + const MessageCount = req.body.length; + + const messageJoined = req.body.join(","); + + const word = MessageCount === 1 ? "subject" : "subjects"; + + if (MessageCount > 0) { + res.send( + `${authPart}\n\nYou have requested information about ${MessageCount} ${word}: ${messageJoined}.`, + ); + } else { + res.send( + `${authPart}\n\nYou have requested information about ${MessageCount} ${word}.`, + ); + } +}); + +app.listen(PORT, () => { + console.log("Type your message here"); +});