Repository: CodeYourFuture/curriculum
Pull request #1388: SDC Decomposition Sprint 3
Author: login: illicitonion
Created: 2025-03-14T16:11:26Z | Merged: 2025-03-29T18:30:47Z



Diff:
diff --git a/common-content/en/module/decomposition/adding-like-dislike/index.md b/common-content/en/module/decomposition/adding-like-dislike/index.md
new file mode 100644
index 000000000..876a9240d
--- /dev/null
+++ b/common-content/en/module/decomposition/adding-like-dislike/index.md
@@ -0,0 +1,60 @@
++++
+title = "Adding like/dislike"
+time = 120
+emoji= "👍👎"
+objectives = [
+    "Identify what data needs to be stored and exchanged between a client and server.",
+    "Devise a scheme for differentiating messages with different meanings (e.g. a new message vs a new like).",
+    "Contrast giving updated values as absolute values or relative changes.",
+    "Implement an end-to-end feature involving data updates and reconciliation across a client and server.",
+]
++++
+
+The last requirement we have for our chat application is the ability to like/dislike a message (and see what messages have been liked/disliked).
+
+{{<note type="Exercise">}}
+Think about what information a client would need to provide to a server in order to like/dislike a message.
+
+Think about what information a server would need to provide to a client in order to display how many likes/dislikes a message has.
+
+Think about what information a server would need to provide to a client in order to _update_ how many likes/dislikes a message has.
+
+Write these things down.
+{{</note>}}
+
+### Identifiers
+
+One of the key new requirements to add liking/disliking a message is knowing _which_ message is being liked/disliked. When a client wants to like a message, it needs some way of saying _this_ is the message I want to like.
+
+This suggests we need a unique identifier for each message:
+* When the server tells a client about a message, it needs to tell it what the identifier is for that message.
+* When a client tells the server it wants to like a message, it needs to tell it the identifier for the message it wants to like.
+* When the server tells a client a message has been liked, it needs to tell the client which message was liked, and the client needs to know enough about that message to be able to update the UI.
+
+### Message formats
+
+Your server is now sending multiple kinds of updates: "Here's a new message" or "Here's an update to the number of likes of an existing message". You will need to make sure the client knows the difference between these messages. Your client will need to know how to act when it receives each kind of message.
+
+### Changes or absolutes?
+
+When new likes happen, the server needs to tell the client about it. We need to choose how the server will tell the client about this. Two options:
+* The server could tell the client "this message was liked again".
+* The server could tell the client "this message now has 10 likes".
+
+Both of these can work.
+
+{{<note type="Exercise">}}
+Write down some advantages and disadvantages of a server -> client update being "+1 compared to before" or "now =10".
+
+Choose which approach you want to take.
+{{</note>}}
+
+{{<note type="Exercise">}}
+Implement liking and disliking messages:
+1. If a message has a non-zero number of likes or dislikes, the frontend needs to show this.
+2. The frontend needs to expose some way for a user to like or dislike any message.
+3. When a user likes or dislikes a message, the frontend needs to tell the backend about this, and the backend needs to notify all clients of this.
+4. When a frontend is notified by a backend about a new like or dislike, it needs to update the UI to show this.
+
+You may do this in your polling implementation, WebSockets implementation, or both.
+{{</note>}}
diff --git a/common-content/en/module/decomposition/continuation-styles/index.md b/common-content/en/module/decomposition/continuation-styles/index.md
new file mode 100644
index 000000000..85b128890
--- /dev/null
+++ b/common-content/en/module/decomposition/continuation-styles/index.md
@@ -0,0 +1,188 @@
++++
+title = "Continuation styles"
+time = 45
+emoji= "➡️"
+objectives = [
+    "Compare different approaches to choosing how to handle potential request errors.",
+]
++++
+
+We extracted functions from our code. We saw three different ways of deciding what to do next:
+
+```js
+app.post("/message", (req, res) => {
+  parseRequestAsJsonObject(req, res, (req, res, body) => {
+    if (!ensureFields(body, res, ["user", "message"])) {
+      return;
+    }
+    receiveMessage(body.user, body.message);
+    res.send("ok");
+  });
+});
+```
+
+1. `app.post` takes a callback which expects two arguments: a request, and a response. It resolves to a completed state when `res.send` is called (which may be done asynchronously).
+1. `parseRequestAsJsonObject` takes a callback very similar to `app.post`'s, but which also takes a `body` parameter.
+1. `ensureFields` itself calls `res.send` if there was an error, and returns a boolean indicating whether the function should keep going or stop.
+
+Each of these approaches is doing something similar, but they're all a little different.
+
+Let's look at a few different ways we could've written `ensureFields` and `parseRequestAsJsonObject`:
+
+## More callbacks
+
+`ensureFields` could've taken a callback to call if the fields were correct:
+
+```js
+app.post("/message", (req, res) => {
+  parseRequestAsJsonObject(req, res, (req, res, body) => {
+    ensureFields(req, res, body, ["user", "message"], (req, res, body) => {
+      receiveMessage(body.user, body.message);
+      res.send("ok");
+    });
+  });
+});
+```
+
+This works, but lots of callbacks can get quite hard to read and follow.
+
+We've seen a similar problem before with asynchronous code. And we've seen Promises, and `async`/`await` as ways of solving that problem.
+
+Unfortunately, the reason we need callbacks here is different, and so can't be solved exactly the same way. The problem here is that a function cannot tell the function that called it to stop running. That's what we're doing in the original code with:
+
+```js
+if (!ensureFields(body, res, ["user", "message"])) {
+  return;
+}
+```
+
+`ensureFields` really wants to tell the calling function "If the fields aren't correct, I've already rejected the request, you should stop running". But it can't automatically do this. So it returns a boolean, and we need to _check_ it and decide to call `return` ourselves.
+
+We could use Promises to manage our callbacks a bit better, but we can't use `async`/`await` because it doesn't have a way to say "return early and stop running".
+
+## Hiding information in existing parameters
+
+`parseRequestAsJsonObject` takes a callback which takes three parameter: `req`, `res`, and `body`.
+
+Alternatively, we could've written `parseRequestAsJsonObject` in such a way that it would add information to the `req` parameter, rather than require an extra one:
+
+```js
+app.post("/message", (req, res) => {
+  parseRequestAsJsonObject(req, res, (req, res) => {
+    ensureFields(req, res, ["user", "message"], (req, res) => {
+      receiveMessage(req.body.user, req.body.message);
+      res.send("ok");
+    });
+  });
+});
+```
+
+Here `parseRequestAsJsonObject` would add a property named `body` to the `req` object it was passed, which callbacks can access as `req.body`.
+
+In some ways, it's really useful to have this third parameter. It means it's obvious to us which functions expect to be able to access the parsed body. It also makes it obvious that we can't use our callback _without_ going via `parseRequestAsJsonObject` - i.e. we can't write:
+
+```js
+app.post("/message", (req, res) => {
+  receiveMessage(body.user, body.message);
+  res.send("ok");
+});
+```
+
+Because there is no variable called `body` - we can _only_ use this in a callback which declares `body`.
+
+In other ways, it's annoying that the {{<tooltip title="Function signature" text="signature of the function">}}The signature of a function is its name, parameters, and return value. It describes how the function is called, and what it produces, without worrying about how it does its work.{{</tooltip>}} has changed. Imagine if we had two different pieces of code here - one which parses the request body as an object, one which gets the user's username based on an authentication token. What order should they add their parameters to the expected callbacks? Would we write:
+
+```js
+app.post("/message", (req, res) => {
+  parseRequestAsJsonObject(req, res, (req, res, body) => {
+    checkAuthentication(req, res, body, (req, res, body, username) => {
+      // Handle the request here
+    });
+  });
+});
+```
+
+Or would we write:
+
+```js
+app.post("/message", (req, res) => {
+  checkAuthentication(req, res, body, (req, res, username) => {
+    parseRequestAsJsonObject(req, res, (req, res, username, body) => {
+      // Handle the request here
+    });
+  });
+});
+```
+
+Because we change the signatures here, the order actually matters. These two functions which extract independent information need to be aware of each other and can only be called in a particular order. Maybe in some of our endpoints we only want to call one of them, but they're now tied together. These {{<tooltip title="Orthogonality" text="orthogonal">}}Orthogonal means unrelated or independent. Parsing the Authorization header is orthogonal to parsing the request body because they can be done separately and don't impact each other.<br /><br />But using the results of those may not be orthogonal, e.g. if we're editing a post, and the request body contains the ID of the post we're trying to edit, the user you're authenticated as may affect whether you're allowed to edit that message.{{</tooltip>}} concepts have become linked.
+
+Modifying the existing parameters (e.g. setting `req.body`) can keep our code more orthogonal. But it also makes some behaviour implicit rather than explicit: taking an argument as a parameter makes it explicit that you expect that parameter to be passed. Just calling `req.body` and hoping `body` has been set _hides_ the requirement that `parseRequestAsJsonObject` was already called. Including requirements in function signatures makes explicit the needs of the function.
+
+## Control over next steps
+
+Let's look at three different ways `ensureFields` could be written:
+
+### Complete control over next steps
+
+`ensureFields` could take a callback to call on success, and could reject the request on failure. It would have complete control over what happens next for this request:
+
+```js
+const ensureFields = (req, res, expectedFields, callback) => {
+  const missingFields = [];
+  for (const expectedField of expectedFields) {
+    if (!(expectedField in req.body)) {
+      missingFields.push(expectedField);
+    }
+  }
+  if (missingFields.length > 0) {
+    const joinedExpectedFields = expectedFields.join(", ");
+    const joinedMissingFields = missingFields.join(", ");
+    console.error(`Failed to extract fields ${joinedMissingFields} from post body: ${bodyString}`);
+    res.status(400).send(`Expected body to be a JSON object containing keys ${joinedExpectedFields} but was missing ${joinedMissingFields}.`);
+  } else {
+    callback(req, res);
+  }
+};
+```
+
+### Responsible for rejecting but not continuing
+
+```js
+const ensureFields = (req, res, expectedFields) => {
+  const missingFields = [];
+  for (const expectedField of expectedFields) {
+    if (!(expectedField in req.body)) {
+      missingFields.push(expectedField);
+    }
+  }
+  if (missingFields.length > 0) {
+    const joinedExpectedFields = expectedFields.join(", ");
+    const joinedMissingFields = missingFields.join(", ");
+    console.error(`Failed to extract fields ${joinedMissingFields} from post body: ${bodyString}`);
+    res.status(400).send(`Expected body to be a JSON object containing keys ${joinedExpectedFields} but was missing ${joinedMissingFields}.`);
+    return false;
+  } else {
+    return true;
+  }
+};
+```
+
+### Responsible for deciding, but not rejecting or continuing
+
+```js
+const ensureFields = (req, res, expectedFields) => {
+  const missingFields = [];
+  for (const expectedField of expectedFields) {
+    if (!(expectedField in req.body)) {
+      missingFields.push(expectedField);
+    }
+  }
+  return missingFields;
+};
+```
+
+{{<note type="Exercise">}}
+Compare these three approaches. What advantages does each have? What problems do they have? When would each be better to choose?
+
+Write down your thoughts.
+{{</note>}}
diff --git a/common-content/en/module/decomposition/different-experiences-for-different-users/index.md b/common-content/en/module/decomposition/different-experiences-for-different-users/index.md
deleted file mode 100644
index 3c70bbf31..000000000
--- a/common-content/en/module/decomposition/different-experiences-for-different-users/index.md
+++ /dev/null
@@ -1,14 +0,0 @@
-+++
-title = "Different experiences for different users"
-headless = true
-time = 30
-emoji= "📖"
-[objectives]
-    1="TODO"
-+++
-
-### Different experiences for different users
-
-TODO: Introduce an "admin" role which gets an additional "delete" button
-
-TODO: Introduce a "view posting stats" page that only the admin can access
diff --git a/common-content/en/module/decomposition/extracting-a-middleware/index.md b/common-content/en/module/decomposition/extracting-a-middleware/index.md
index 6cf361a63..be1121c25 100644
--- a/common-content/en/module/decomposition/extracting-a-middleware/index.md
+++ b/common-content/en/module/decomposition/extracting-a-middleware/index.md
@@ -1,12 +1,68 @@
 +++
 title = "Extracting a middleware"
-headless = true
-time = 30
-emoji= "📖"
-[objectives]
-    1="Identify and explain the risks and costs of manually calling code (e.g. authentication validators/extractors) from multiple route handlers"
-    2="Explain the middleware pattern"
-    3="Implement and wire up a middleware"
+time = 105
+emoji= "⛓️"
+objectives = [
+    "Identify and explain the risks and costs of manually calling code (e.g. authentication validators/extractors) from multiple route handlers.",
+    "Explain the middleware pattern.",
+    "Implement and wire up a middleware with Express.",
+]
 +++
 
-### Extracting a middleware
+It is very common that in an application there are operations we want to perform for most or all requests.
+
+For instance, we often want to check authentication details for _every_ request to an endpoint which requires authentication.
+
+And typically we use the same format for all POST request bodies to an application (often JSON, but some applications use other formats) - we want to parse _all_ POST request bodies in that format.
+
+It can be annoying to have to write the same code (like calling `parseRequestAsJsonObject`) in every handler. It can also be **dangerous** to require doing so:
+
+> [!WARNING]
+>
+> If we forget to call a function to check a user is logged in in one endpoint, that may be a big security problem.
+
+One strategy to improve this is to use a {{<tooltip title="Middleware" text="middleware">}}A middleware is a piece of code which is called before route handlers.{{</tooltip>}}. A middleware may process the request, do extra checks (e.g. check an authorization token), or attach extra data (e.g. parsing a POST body as JSON and adding the fields to the request object). It can choose to respond to the request itself, or allow the route handler to do so.
+
+{{<note type="Reading">}}
+Read the [Express documentation on writing middleware](https://expressjs.com/en/guide/writing-middleware.html).
+{{</note>}}
+
+{{<note type="Exercise">}}
+Write a tiny Express application. You must write two separate middlewares.
+
+Requirements:
+* There must be an endpoint which handles POST requests.
+* A middleware should look for a header with name `X-Username`. If this is set, it will modify `req` to add a `username` property set to this value. If it is not set, the property should be set to `null`.
+* A middleware should parse the request POST body as a JSON array. It should modify `req` to add a `body` property to this value. If the POST body was not a JSON array, or the array contains non-string elements, it should reject the request.
+* The response should look like:
+
+```
+You are authenticated as Ahmed.
+
+You have requested information about 4 subjects: Birds, Bats, Lizards, Bees.
+```
+
+or
+
+```
+You are not authenticated.
+
+You have requested information about 1 subject: Bees.
+```
+
+or
+
+```
+You are authenticated as Gemma.
+
+You have requested information about 0 subjects.
+```
+
+You can test your application by running some `curl` commands like:
+
+```console
+% curl -X POST --data '["Bees"]' -H "X-Username: Ahmed" http://localhost:3000
+You are authenticated as Ahmed.
+
+You have requested information about 1 subject: Bees.
+{{</note>}}
diff --git a/common-content/en/module/decomposition/extracting-common-functionality/index.md b/common-content/en/module/decomposition/extracting-common-functionality/index.md
index 4f3f2e804..e8c8fafb2 100644
--- a/common-content/en/module/decomposition/extracting-common-functionality/index.md
+++ b/common-content/en/module/decomposition/extracting-common-functionality/index.md
@@ -1,13 +1,95 @@
 +++
 title = "Extracting common functionality"
-headless = true
 time = 30
-emoji= "📖"
-[objectives]
-    1="Identify and explain the risks and costs of duplicate code"
-    2="Extract functionality used by multiple route handlers into a common function"
+emoji= "✂️"
+objectives = [
+    "Extract functionality used by multiple route handlers into a common function.",
+]
 +++
 
-### Extracting common functionality
+Having identified these three parts of our handlers, we can extract functions to make them more clear.
 
-TODO: Extract common code to determine whether the current user is an admin across multiple views
+One way we could do this is:
+
+```js {linenos=table}
+app.post("/message", (req, res) => {
+  parseRequestAsJsonObject(req, res, (req, res, body) => {
+    if (!ensureFields(body, res, ["user", "message"])) {
+      return;
+    }
+    receiveMessage(body.user, body.message);
+    res.send("ok");
+  });
+});
+
+app.post("/react", (req, res) => {
+  parseRequestAsJsonObject(req, res, (req, res, body) => {
+    if (!ensureFields(body, res, ["id", "reaction"])) {
+      return;
+    }
+    if (!(body.id in messageReactions)) {
+      res.status(404).send(`Got reaction to message id ${body.id} which doesn't exist.`);
+      return;
+    }
+    const reactionsForMessage = messageReactions[body.id];
+    if (!(body.reaction) in reactionsForMessage) {
+      res.status(400).send(`Reaction ${body.reaction} isn't allowed.`);
+      return;
+    }
+    reactionsForMessage[body.reaction]++;
+
+    res.send("ok");
+  });
+});
+
+const parseRequestAsJsonObject = (req, res, callback) => {
+  const bodyBytes = [];
+  req.on("data", chunk => bodyBytes.push(...chunk));
+  req.on("end", () => {
+    const bodyString = String.fromCharCode(...bodyBytes);
+    let body;
+    try {
+      body = JSON.parse(bodyString);
+    } catch (error) {
+      console.error(`Failed to parse body ${bodyString} as JSON: ${error}`);
+      res.status(400).send("Expected body to be JSON.");
+      return;
+    }
+    if (typeof body !== "object") {
+      console.error(`Got POST body which was not an object: ${bodyString}`);
+      res.status(400).send("Expected body to be a JSON object.");
+      return;
+    }
+    callback(req, res, body);
+  });
+}
+
+const ensureFields = (body, res, expectedFields) => {
+  const missingFields = [];
+  for (const expectedField of expectedFields) {
+    if (!(expectedField in body)) {
+      missingFields.push(expectedField);
+    }
+  }
+  if (missingFields.length > 0) {
+    const joinedExpectedFields = expectedFields.join(", ");
+    const joinedMissingFields = missingFields.join(", ");
+    console.error(`Failed to extract fields ${joinedMissingFields} from post body: ${bodyString}`);
+    res.status(400).send(`Expected body to be a JSON object containing keys ${joinedExpectedFields} but was missing ${joinedMissingFields}.`);
+    return false;
+  }
+  return true;
+};
+```
+
+## Why is this better?
+
+Before, the `/message` function body was 20 lines. 18 were just for processing the post body, and 2 were for the actual functionality. 10% of the function was for relevant functionality.
+
+Now, the function body is 7 lines. They say: "I expect the request to be a JSON object", "I expect these fields in the object", and then do the relevant functionality.
+
+We can see at a glance what the function does. And the common functionality has a clear name explaining what it's doing, rather than needing us to read 18 lines to work it out.
+
+We can also more easily see the _difference_ between what the two endpoints do. Each endpoint expects the request to be a JSON object. They both expect some fields, but different fields. Then they do something different with them. This is much easier to see than before, where a subtle difference may have been hard to notice.
+
+We can also ignore the details of the helper functions if we don't need to know them. If we don't care exactly how the body is parsed as JSON, we can just trust that the function does it somehow. If we _do_ need to know, we can look into the function. If we need to change or fix something, we only need to do it in one place.
diff --git a/common-content/en/module/decomposition/identifying-common-functionality/index.md b/common-content/en/module/decomposition/identifying-common-functionality/index.md
new file mode 100644
index 000000000..8942b2117
--- /dev/null
+++ b/common-content/en/module/decomposition/identifying-common-functionality/index.md
@@ -0,0 +1,266 @@
++++
+title = "Identifying common functionality"
+headless = true
+time = 30
+emoji= "🔎"
+objectives = [
+    "Identify and explain the risks and costs of duplicate code.",
+    "Identify which code is equivalent, equivalent with parameters, or unique.",
+]
++++
+
+Often in different HTTP handlers we do the same thing. This happens because we use similar patterns. For instance, we may expect all of our request bodies to contain JSON objects. Or we may expect all requests from signed in users to have an `Authorization` header in a certain format.
+
+For instance, an implementation of "receiving messages" and "receiving reactions to messages" may look like this:
+
+```js {linenos=table}
+app.post("/message", (req, res) => {
+  const bodyBytes = [];
+  req.on("data", chunk => bodyBytes.push(...chunk));
+  req.on("end", () => {
+    const bodyString = String.fromCharCode(...bodyBytes);
+    let body;
+    try {
+      body = JSON.parse(bodyString);
+    } catch (error) {
+      console.error(`Failed to parse body ${bodyString} as JSON: ${error}`);
+      res.status(400).send("Expected body to be JSON.");
+      return;
+    }
+    if (typeof body !== "object" || !("user" in body) || !("message" in body)) {
+      console.error(`Failed to extract user and message from post body: ${bodyString}`);
+      res.status(400).send("Expected body to be a JSON object containing keys user and message.");
+      return;
+    }
+    receiveMessage(body.user, body.message);
+    res.send("ok");
+  });
+});
+
+app.post("/react", (req, res) => {
+  const bodyBytes = [];
+  req.on("data", chunk => bodyBytes.push(...chunk));
+  req.on("end", () => {
+    const bodyString = String.fromCharCode(...bodyBytes);
+    let body;
+    try {
+      body = JSON.parse(bodyString);
+    } catch (error) {
+      console.error(`Failed to parse body ${bodyString} as JSON: ${error}`);
+      res.status(400).send("Expected body to be JSON.");
+      return;
+    }
+    if (typeof body !== "object" || !("id" in body) || !("reaction" in body)) {
+      console.error(`Failed to extract id and reaction from post body: ${bodyString}`);
+      res.status(400).send("Expected body to be a JSON object containing keys id and reaction.");
+      return;
+    }
+    if (!(body.id in messageReactions)) {
+      res.status(404).send(`Got reaction to message id ${body.id} which doesn't exist.`);
+      return;
+    }
+    const reactionsForMessage = messageReactions[body.id];
+    if (!(body.reaction) in reactionsForMessage) {
+      res.status(400).send(`Reaction ${body.reaction} isn't allowed.`);
+      return;
+    }
+    reactionsForMessage[body.reaction]++;
+
+    res.send("ok");
+  });
+});
+```
+
+(It's ok if your implementation looked different, but we're going to talk about this example).
+
+Both of these POST handlers mostly contain code for parsing a response body as JSON.
+
+This causes a few problems:
+1. Imagine we found a bug in our parsing - we'd need to fix that bug in multiple places.
+1. It's really hard to tell, at a glance, what each handler is actually doing.
+
+Let's try to work out what is specific to each handler, and what is more general shared behaviour.
+
+## Specific vs shared behaviours
+
+Both handlers accumulate chunks of the request as a string, then parse them as a JSON object, returning an error if this failed:
+
+```js {linenos=table,hl_lines=["2-13", "25-36"]}
+app.post("/message", (req, res) => {
+  const bodyBytes = [];
+  req.on("data", chunk => bodyBytes.push(...chunk));
+  req.on("end", () => {
+    const bodyString = String.fromCharCode(...bodyBytes);
+    let body;
+    try {
+      body = JSON.parse(bodyString);
+    } catch (error) {
+      console.error(`Failed to parse body ${bodyString} as JSON: ${error}`);
+      res.status(400).send("Expected body to be JSON.");
+      return;
+    }
+    if (typeof body !== "object" || !("user" in body) || !("message" in body)) {
+      console.error(`Failed to extract user and message from post body: ${bodyString}`);
+      res.status(400).send("Expected body to be a JSON object containing keys user and message.");
+      return;
+    }
+    receiveMessage(body.user, body.message);
+    res.send("ok");
+  });
+});
+
+app.post("/react", (req, res) => {
+  const bodyBytes = [];
+  req.on("data", chunk => bodyBytes.push(...chunk));
+  req.on("end", () => {
+    const bodyString = String.fromCharCode(...bodyBytes);
+    let body;
+    try {
+      body = JSON.parse(bodyString);
+    } catch (error) {
+      console.error(`Failed to parse body ${bodyString} as JSON: ${error}`);
+      res.status(400).send("Expected body to be JSON.");
+      return;
+    }
+    if (typeof body !== "object" || !("id" in body) || !("reaction" in body)) {
+      console.error(`Failed to extract id and reaction from post body: ${bodyString}`);
+      res.status(400).send("Expected body to be a JSON object containing keys id and reaction.");
+      return;
+    }
+    if (!(body.id in messageReactions)) {
+      res.status(404).send(`Got reaction to message id ${body.id} which doesn't exist.`);
+      return;
+    }
+    const reactionsForMessage = messageReactions[body.id];
+    if (!(body.reaction) in reactionsForMessage) {
+      res.status(400).send(`Reaction ${body.reaction} isn't allowed.`);
+      return;
+    }
+    reactionsForMessage[body.reaction]++;
+
+    res.send("ok");
+  });
+});
+```
+
+Both implementations check that the object contains certain keys, but they each check for different specific keys. These are doing the same thing, but with different parameters:
+
+```js {linenos=table,hl_lines=["14-18", "37-41"]}
+app.post("/message", (req, res) => {
+  const bodyBytes = [];
+  req.on("data", chunk => bodyBytes.push(...chunk));
+  req.on("end", () => {
+    const bodyString = String.fromCharCode(...bodyBytes);
+    let body;
+    try {
+      body = JSON.parse(bodyString);
+    } catch (error) {
+      console.error(`Failed to parse body ${bodyString} as JSON: ${error}`);
+      res.status(400).send("Expected body to be JSON.");
+      return;
+    }
+    if (typeof body !== "object" || !("user" in body) || !("message" in body)) {
+      console.error(`Failed to extract user and message from post body: ${bodyString}`);
+      res.status(400).send("Expected body to be a JSON object containing keys user and message.");
+      return;
+    }
+    receiveMessage(body.user, body.message);
+    res.send("ok");
+  });
+});
+
+app.post("/react", (req, res) => {
+  const bodyBytes = [];
+  req.on("data", chunk => bodyBytes.push(...chunk));
+  req.on("end", () => {
+    const bodyString = String.fromCharCode(...bodyBytes);
+    let body;
+    try {
+      body = JSON.parse(bodyString);
+    } catch (error) {
+      console.error(`Failed to parse body ${bodyString} as JSON: ${error}`);
+      res.status(400).send("Expected body to be JSON.");
+      return;
+    }
+    if (typeof body !== "object" || !("id" in body) || !("reaction" in body)) {
+      console.error(`Failed to extract id and reaction from post body: ${bodyString}`);
+      res.status(400).send("Expected body to be a JSON object containing keys id and reaction.");
+      return;
+    }
+    if (!(body.id in messageReactions)) {
+      res.status(404).send(`Got reaction to message id ${body.id} which doesn't exist.`);
+      return;
+    }
+    const reactionsForMessage = messageReactions[body.id];
+    if (!(body.reaction) in reactionsForMessage) {
+      res.status(400).send(`Reaction ${body.reaction} isn't allowed.`);
+      return;
+    }
+    reactionsForMessage[body.reaction]++;
+
+    res.send("ok");
+  });
+});
+```
+
+Then each implementation does something quite different. The highlighted code is very specific to what the endpoint is for:
+
+```js {linenos=table,hl_lines=["19-20", "42-53"]}
+app.post("/message", (req, res) => {
+  const bodyBytes = [];
+  req.on("data", chunk => bodyBytes.push(...chunk));
+  req.on("end", () => {
+    const bodyString = String.fromCharCode(...bodyBytes);
+    let body;
+    try {
+      body = JSON.parse(bodyString);
+    } catch (error) {
+      console.error(`Failed to parse body ${bodyString} as JSON: ${error}`);
+      res.status(400).send("Expected body to be JSON.");
+      return;
+    }
+    if (typeof body !== "object" || !("user" in body) || !("message" in body)) {
+      console.error(`Failed to extract user and message from post body: ${bodyString}`);
+      res.status(400).send("Expected body to be a JSON object containing keys user and message.");
+      return;
+    }
+    receiveMessage(body.user, body.message);
+    res.send("ok");
+  });
+});
+
+app.post("/react", (req, res) => {
+  const bodyBytes = [];
+  req.on("data", chunk => bodyBytes.push(...chunk));
+  req.on("end", () => {
+    const bodyString = String.fromCharCode(...bodyBytes);
+    let body;
+    try {
+      body = JSON.parse(bodyString);
+    } catch (error) {
+      console.error(`Failed to parse body ${bodyString} as JSON: ${error}`);
+      res.status(400).send("Expected body to be JSON.");
+      return;
+    }
+    if (typeof body !== "object" || !("id" in body) || !("reaction" in body)) {
+      console.error(`Failed to extract id and reaction from post body: ${bodyString}`);
+      res.status(400).send("Expected body to be a JSON object containing keys id and reaction.");
+      return;
+    }
+    if (!(body.id in messageReactions)) {
+      res.status(404).send(`Got reaction to message id ${body.id} which doesn't exist.`);
+      return;
+    }
+    const reactionsForMessage = messageReactions[body.id];
+    if (!(body.reaction) in reactionsForMessage) {
+      res.status(400).send(`Reaction ${body.reaction} isn't allowed.`);
+      return;
+    }
+    reactionsForMessage[body.reaction]++;
+
+    res.send("ok");
+  });
+});
+```
+
+Ideally, most of the code in these functions would be specific to the behaviour of the handler. It would be easy to see each function does from reading them.
diff --git a/common-content/en/module/decomposition/using-existing-middleware/index.md b/common-content/en/module/decomposition/using-existing-middleware/index.md
new file mode 100644
index 000000000..3ddb58d5e
--- /dev/null
+++ b/common-content/en/module/decomposition/using-existing-middleware/index.md
@@ -0,0 +1,20 @@
++++
+title = "Using existing middleware"
+time = 30
+emoji= "⛓️"
+objectives = [
+    "Use an existing middleware with Express.",
+]
++++
+
+We don't always need to write our own middlewares. For common tasks, such as parsing JSON request bodies, there are existing middlewares we can re-use.
+
+{{<note type="Exercise">}}
+Make a copy of your previous middleware application.
+
+Delete the middleware you wrote that handles the JSON request POST body.
+
+Switch to instead the JSON middleware built in to Express.
+
+Debug why the `curl` command suggested in the previous exercise doesn't work. Fix this problem by modifying the `curl` command.
+{{</note>}}
diff --git a/common-content/en/module/decomposition/websockets/index.md b/common-content/en/module/decomposition/websockets/index.md
new file mode 100644
index 000000000..277be210c
--- /dev/null
+++ b/common-content/en/module/decomposition/websockets/index.md
@@ -0,0 +1,116 @@
++++
+title = "WebSockets"
+time = 120
+emoji= "🔌"
+objectives = [
+    "Stream live updates from a server using WebSockets.",
+    "Discuss the trade-offs of using WebSockets or polling.",
+    "Describe properties of message formats (e.g. including a command name).",
+]
++++
+
+> [WebSockets ](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) are an API and protocol which create a two-way communication channel between two programs.
+
+Websockets are used to establish a channel so that a backend can send updates to a frontend website.
+
+You can read [an introduction to WebSockets](https://docs.developer.tech.gov.sg/docs/data-engineering-initiative-playbook/Chapter5/Introduction_to_WebSockets), as well as roughly [what a client looks like](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_client_applications), and [what a server does](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers).
+
+On the server side, we will be using the [websocket npm package](https://www.npmjs.com/package/websocket) which lists a server example in its README.
+
+> [!TIP]
+>
+> This sprint, you will need to submit _both_ a copy of your code which supports polling, _and_ a copy which supports WebSockets.
+>
+> You probably want to make a copy of your polling code, and have two separate (similar) pages in your repo.
+>
+> Note that this is different from other work you've done, where you make different branches for different pieces of work. We want both of your implementations on one branch.
+>
+> You should aim to share code between these implementations where possible.
+
+On the backend, create a WebSocket server by adding this code:
+
+```js
+import { server as WebSocketServer } from "websocket";
+const server = http.createServer(app);
+const webSocketServer = new WebSocketServer({ httpServer: server });
+```
+
+Next, follow the example in the `websocket` npm package's documentation to have your server handle requests.
+
+On the client-side, you will need to make a new `WebSocket` connection to the server.
+
+Some things to think about when implementing WebSockets updates:
+
+### Learn new APIs in isolation
+
+It will be easier for you to learn a new API (like WebSockets) with a simple example.
+
+Your goal is to make a WebSocket to stream new messages from the server to the client. Your existing codebase is more complicated and handles many other concerns. To explore WebSockets, configure the server to always report the message "Hello". This way, you can isolate the new process and test things out more easily.
+
+You could even write a whole new website which _only_ makes a WebSocket connection and displays a message.
+
+Once you have an example WebSocket working, and understand how it works, keep going. Apply your new understanding to the real problem you're trying to solve.
+
+### Think about the protocol you want
+
+WebSockets let you send arbitrary text (or binary) messages.
+
+In our quote server, we switched from our backend returning a pre-formatted string of a quote, to returning a JSON object so we could get the parts ourselves.
+
+Think about what structure would be useful for our client and our server to know about.
+
+If we're going to add more messages in the future (e.g. for "liking" a message), how will the receiver of the message know what kind of message the one it receives is?
+
+One thing we often do is wrap our message in an object, with a field specifically saying what the command is.
+
+e.g. instead of sending:
+```json
+{
+    "user": "Azin",
+    "message": "Hello!"
+}
+```
+
+we may send:
+
+```json
+{
+    "command": "send-message",
+    "message": {
+        "user": "Azin",
+        "message": "Hello!"
+    }
+}
+```
+
+This means that if we add new commands in the future, we don't need to change our existing code.
+
+### Think about timings
+
+When we first load a page, we need to get all of the messages that already exist.
+
+After that, we can ask to be notified of new messages.
+
+There are a few ways we could do that. An interesting question is what happens _between_ these events?
+
+Imagine we made an HTTP GET request to ask for all of the messages, then created a WebSocket to get new messages. What happens if someone sent a message between when we got our response, and when the WebSocket was connected? How can we make sure we don't miss any messages?
+
+Or imagine we made a WebSocket request, and expected to receive a list of all previous messages, and then to keep receiving updates. Does the server need to remember which messages have already been sent to each client?
+
+{{<note type="Exercise">}}
+How will you make sure that your client won't miss any new messages, after getting the initial messages? Write down your strategy.
+{{</note>}}
+
+### Remember WebSockets are bi-directional
+
+Now, we're using a `POST` request to send a new message, and a `WebSocket` to stream receiving new messages. But we know that WebSockets are bi-directional - we can both send and receive information on them. We could change our sending logic to also use our WebSocket. Or we could keep using HTTP POST requests. Both of these approaches work.
+
+{{<note type="Exercise">}}
+Think: What advantages does each approach have?
+
+Why might we want to change our implementation to use a WebSocket for sending messages?
+
+Why might we want to keep using POST requests for sending messages?
+
+Why might we want to support _both_ on our server? Why might we only want to support one on our server?
+{{</note>}}
diff --git a/org-cyf-sdc/content/decomposition/sprints/3/_index.md b/org-cyf-sdc/content/decomposition/sprints/3/_index.md
index cd87b5e8b..df042f4bd 100644
--- a/org-cyf-sdc/content/decomposition/sprints/3/_index.md
+++ b/org-cyf-sdc/content/decomposition/sprints/3/_index.md
@@ -5,5 +5,5 @@ layout = 'sprint'
 emoji= '⏱️'
 menu_level = ['module']
 weight = 3
-theme = "Middleware: Extracting common functionality"
+theme = "Websockets and Middlewares"
 +++
diff --git a/org-cyf-sdc/content/decomposition/sprints/3/prep/index.md b/org-cyf-sdc/content/decomposition/sprints/3/prep/index.md
index e482b33ee..c314b5986 100644
--- a/org-cyf-sdc/content/decomposition/sprints/3/prep/index.md
+++ b/org-cyf-sdc/content/decomposition/sprints/3/prep/index.md
@@ -6,12 +6,24 @@ emoji= '🧑🏾‍💻'
 menu_level = ['sprint']
 weight = 1
 [[blocks]]
-title = "Different experiences for different users"
-src = "module/decomposition/different-experiences-for-different-users"
+name = "Websockets"
+src = "module/decomposition/websockets"
 [[blocks]]
-title = "Extracting common functionality"
+name = "Adding like/dislike"
+src = "module/decomposition/adding-like-dislike"
+[[blocks]]
+name = "Identifying common functionality"
+src = "module/decomposition/identifying-common-functionality"
+[[blocks]]
+name = "Extracting common functionality"
 src = "module/decomposition/extracting-common-functionality"
 [[blocks]]
-title = "Extracting a middleware"
+name = "Continuation styles"
+src = "module/decomposition/continuation-styles"
+[[blocks]]
+name = "Extracting a middleware"
 src = "module/decomposition/extracting-a-middleware"
+[[blocks]]
+name = "Using existing middleware"
+src = "module/decomposition/using-existing-middleware"
 +++


Reviews:
user: login: SallyMcGrath
body: Really great work here. Thank you so much for your hard work on this. Minor notes mainly about chunking text and untangling some sentences.

Looking great - I have tried to bring out ideas from this into the Legacy code module so they reinforce each other (without making them tightly coupled or sequenced)

As for the level - we'll find out. We will likely need to scale it back a bit but I think it's not harmful to try this at least once. 
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-21T22:02:43Z
user: login: illicitonion
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-25T12:12:01Z
user: login: SallyMcGrath
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-25T13:26:08Z
user: login: illicitonion
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-25T15:23:22Z

Review comments:
diff_hunk: @@ -0,0 +1,62 @@
++++
+title = "Adding like/dislike"
+time = 120
+emoji= "👍👎"
+objectives = [
+    "Identify what data needs to be stored and exchanged between a client and server.",
+    "Devise a scheme for differentiating messages with different meanings (e.g. a new message vs a new like).",
+    "Contrast giving updated values as absolute values or relative changes.",
+    "Implement an end-to-end feature involving data updates and reconciliation across a client and server.",
+]
++++
+
+The last requirement we have for our chat application is the ability to like/dislike a message (and see what messages have been liked/disliked).
+
+{{<note type="Exercise">}}
+Think about what information a client would need to provide to a server in order to like/dislike a message.
+
+Think about what information a server would need to provide to a client in order to display how many likes/dislikes a message has.
+
+Think about what information a server would need to provide to a client in order to _update_ how many likes/dislikes a message has.
+
+Write these things down.
+{{</note>}}
+
+### Identifiers
+
+One of the key new requirements to add liking/disliking a message is knowing _which_ message is being liked/disliked.
+
+When a client wants to like a message, it needs some way of saying _this_ is the message I want to like.
+
+This suggests we need a unique identifier for each message:
+* When the server tells a client about a message, it needs to tell it what the identifier is for that message.
+* When a client tells the server it wants to like a message, it needs to tell it the identifier for the message it wants to like.
+* When the server tells a client a message has been liked, it needs to tell the client which message was liked, and the client needs to know enough about that message to be able to update the UI.
+
+### Message formats
+
+Now that your server will be sending multiple kinds of updates ("Here's a new message", or "Here's an update to the number of likes of an existing message"), you'll need to make sure the client knows the difference between these messages. The client will need to know how to act when it receives each kind of message.
+
+### Changes or absolutes?
+
+When new likes happen, a choice we need to make is whether the server should tell a client "this message was liked again" or should tell the client "this message now has 10 likes". Both of these can work.
path: common-content/en/module/decomposition/adding-like-dislike/index.md
user: login: SallyMcGrath
body: ```suggestion
When new likes happen, we must describe that change. Our server could tell a client "this message was liked again" or it could tell the client "this message now has 10 likes". Either of these can work.
```
created_at: 2025-03-14T16:44:08Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,114 @@
++++
+title = "WebSockets"
+time = 120
+emoji= "🔌"
+objectives = [
+    "Stream live updates from a server using WebSockets.",
+    "Discuss the trade-offs of using WebSockets or polling.",
+    "Describe properties of message formats (e.g. including a command name).",
+]
++++
+
+WebSockets are an API and protocol which allow creating a bi-directional communication channel between two programs.
path: common-content/en/module/decomposition/websockets/index.md
user: login: SallyMcGrath
body: ```suggestion
> [WebSockets ](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) are an API and protocol which create a two-way communication channel between two programs.
```
created_at: 2025-03-21T21:24:56Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,114 @@
++++
+title = "WebSockets"
+time = 120
+emoji= "🔌"
+objectives = [
+    "Stream live updates from a server using WebSockets.",
+    "Discuss the trade-offs of using WebSockets or polling.",
+    "Describe properties of message formats (e.g. including a command name).",
+]
++++
+
+WebSockets are an API and protocol which allow creating a bi-directional communication channel between two programs.
+
+They are commonly used in websites to establish a channel so that a backend can send updates to a frontend.
path: common-content/en/module/decomposition/websockets/index.md
user: login: SallyMcGrath
body: ```suggestion
Websockets are used to establish a channel so that a backend can send updates to a frontend website.
```
created_at: 2025-03-21T21:25:32Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,114 @@
++++
+title = "WebSockets"
+time = 120
+emoji= "🔌"
+objectives = [
+    "Stream live updates from a server using WebSockets.",
+    "Discuss the trade-offs of using WebSockets or polling.",
+    "Describe properties of message formats (e.g. including a command name).",
+]
++++
+
+WebSockets are an API and protocol which allow creating a bi-directional communication channel between two programs.
+
+They are commonly used in websites to establish a channel so that a backend can send updates to a frontend.
+
+You can read [an introduction to WebSockets](https://docs.developer.tech.gov.sg/docs/data-engineering-initiative-playbook/Chapter5/Introduction_to_WebSockets), as well as roughly [what a client looks like](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_client_applications), and [what a server does](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers).
+
+On the server side, we will be using the [websocket npm package](https://www.npmjs.com/package/websocket) which lists a server example in its README.
+
+> [!TIP]
+>
+> This sprint, you will need to submit _both_ a copy of your code which supports polling, _and_ a copy which supports WebSockets.
path: common-content/en/module/decomposition/websockets/index.md
user: login: SallyMcGrath
body: You don't want them to work in separate branches?
created_at: 2025-03-21T21:26:24Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,114 @@
++++
+title = "WebSockets"
+time = 120
+emoji= "🔌"
+objectives = [
+    "Stream live updates from a server using WebSockets.",
+    "Discuss the trade-offs of using WebSockets or polling.",
+    "Describe properties of message formats (e.g. including a command name).",
+]
++++
+
+WebSockets are an API and protocol which allow creating a bi-directional communication channel between two programs.
+
+They are commonly used in websites to establish a channel so that a backend can send updates to a frontend.
+
+You can read [an introduction to WebSockets](https://docs.developer.tech.gov.sg/docs/data-engineering-initiative-playbook/Chapter5/Introduction_to_WebSockets), as well as roughly [what a client looks like](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_client_applications), and [what a server does](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers).
+
+On the server side, we will be using the [websocket npm package](https://www.npmjs.com/package/websocket) which lists a server example in its README.
+
+> [!TIP]
+>
+> This sprint, you will need to submit _both_ a copy of your code which supports polling, _and_ a copy which supports WebSockets.
+>
+> You probably want to make a copy of your polling code, and have two separate (similar) pages in your repo.
+
+On the backend, you can create a WebSocket server by adding this code:
path: common-content/en/module/decomposition/websockets/index.md
user: login: SallyMcGrath
body: ```suggestion
On the backend, create a WebSocket server by adding this code:
```
created_at: 2025-03-21T21:26:38Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,114 @@
++++
+title = "WebSockets"
+time = 120
+emoji= "🔌"
+objectives = [
+    "Stream live updates from a server using WebSockets.",
+    "Discuss the trade-offs of using WebSockets or polling.",
+    "Describe properties of message formats (e.g. including a command name).",
+]
++++
+
+WebSockets are an API and protocol which allow creating a bi-directional communication channel between two programs.
+
+They are commonly used in websites to establish a channel so that a backend can send updates to a frontend.
+
+You can read [an introduction to WebSockets](https://docs.developer.tech.gov.sg/docs/data-engineering-initiative-playbook/Chapter5/Introduction_to_WebSockets), as well as roughly [what a client looks like](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_client_applications), and [what a server does](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers).
+
+On the server side, we will be using the [websocket npm package](https://www.npmjs.com/package/websocket) which lists a server example in its README.
+
+> [!TIP]
+>
+> This sprint, you will need to submit _both_ a copy of your code which supports polling, _and_ a copy which supports WebSockets.
+>
+> You probably want to make a copy of your polling code, and have two separate (similar) pages in your repo.
+
+On the backend, you can create a WebSocket server by adding this code:
+
+```js
+import { server as WebSocketServer } from "websocket";
+const server = http.createServer(app);
+const webSocketServer = new WebSocketServer({ httpServer: server });
+```
+
+You will then need to follow the example in the `websocket` npm package's documentation to have your server handle requests.
path: common-content/en/module/decomposition/websockets/index.md
user: login: SallyMcGrath
body: ```suggestion
Next, follow the example in the `websocket` npm package's documentation to have your server handle requests.
```
created_at: 2025-03-21T21:26:53Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,114 @@
++++
+title = "WebSockets"
+time = 120
+emoji= "🔌"
+objectives = [
+    "Stream live updates from a server using WebSockets.",
+    "Discuss the trade-offs of using WebSockets or polling.",
+    "Describe properties of message formats (e.g. including a command name).",
+]
++++
+
+WebSockets are an API and protocol which allow creating a bi-directional communication channel between two programs.
+
+They are commonly used in websites to establish a channel so that a backend can send updates to a frontend.
+
+You can read [an introduction to WebSockets](https://docs.developer.tech.gov.sg/docs/data-engineering-initiative-playbook/Chapter5/Introduction_to_WebSockets), as well as roughly [what a client looks like](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_client_applications), and [what a server does](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers).
+
+On the server side, we will be using the [websocket npm package](https://www.npmjs.com/package/websocket) which lists a server example in its README.
+
+> [!TIP]
+>
+> This sprint, you will need to submit _both_ a copy of your code which supports polling, _and_ a copy which supports WebSockets.
+>
+> You probably want to make a copy of your polling code, and have two separate (similar) pages in your repo.
+
+On the backend, you can create a WebSocket server by adding this code:
+
+```js
+import { server as WebSocketServer } from "websocket";
+const server = http.createServer(app);
+const webSocketServer = new WebSocketServer({ httpServer: server });
+```
+
+You will then need to follow the example in the `websocket` npm package's documentation to have your server handle requests.
+
+On the client-side, you will need to make a new `WebSocket` connection to the server.
+
+Some things to think about when implementing WebSockets updates:
+
+### Learn new APIs in isolation
+
+It will be easier for you to learn a new API (like WebSockets) with a simple example.
+
+At the end, you will want your WebSocket to stream new messages from the server to the client, but maybe to explore WebSockets you want the server to always report the message "Hello" when it's connected to, so you can test things out more easily? Or even write a whole new website which _only_ makes a WebSocket connection and displays a message?
path: common-content/en/module/decomposition/websockets/index.md
user: login: SallyMcGrath
body: ```suggestion
Your goal is to make a WebSocket to stream new messages from the server to the client. Your existing codebase is more complicated and handles many other concerns. To explore WebSockets, configure the server to always report the message "Hello". This way, you can isolate the new process and test things out more easily.

You could even write a whole new website which _only_ makes a WebSocket connection and displays a message.
```
created_at: 2025-03-21T21:33:26Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,114 @@
++++
+title = "WebSockets"
+time = 120
+emoji= "🔌"
+objectives = [
+    "Stream live updates from a server using WebSockets.",
+    "Discuss the trade-offs of using WebSockets or polling.",
+    "Describe properties of message formats (e.g. including a command name).",
+]
++++
+
+WebSockets are an API and protocol which allow creating a bi-directional communication channel between two programs.
+
+They are commonly used in websites to establish a channel so that a backend can send updates to a frontend.
+
+You can read [an introduction to WebSockets](https://docs.developer.tech.gov.sg/docs/data-engineering-initiative-playbook/Chapter5/Introduction_to_WebSockets), as well as roughly [what a client looks like](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_client_applications), and [what a server does](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers).
+
+On the server side, we will be using the [websocket npm package](https://www.npmjs.com/package/websocket) which lists a server example in its README.
+
+> [!TIP]
+>
+> This sprint, you will need to submit _both_ a copy of your code which supports polling, _and_ a copy which supports WebSockets.
+>
+> You probably want to make a copy of your polling code, and have two separate (similar) pages in your repo.
+
+On the backend, you can create a WebSocket server by adding this code:
+
+```js
+import { server as WebSocketServer } from "websocket";
+const server = http.createServer(app);
+const webSocketServer = new WebSocketServer({ httpServer: server });
+```
+
+You will then need to follow the example in the `websocket` npm package's documentation to have your server handle requests.
+
+On the client-side, you will need to make a new `WebSocket` connection to the server.
+
+Some things to think about when implementing WebSockets updates:
+
+### Learn new APIs in isolation
+
+It will be easier for you to learn a new API (like WebSockets) with a simple example.
+
+At the end, you will want your WebSocket to stream new messages from the server to the client, but maybe to explore WebSockets you want the server to always report the message "Hello" when it's connected to, so you can test things out more easily? Or even write a whole new website which _only_ makes a WebSocket connection and displays a message?
+
+Once you have an example WebSocket working, and understand how it works, it should be easier for you to apply that to the real problem you're trying to solve.
path: common-content/en/module/decomposition/websockets/index.md
user: login: SallyMcGrath
body: ```suggestion
Once you have an example WebSocket working, and understand how it works, move forward. Apply your new understanding to the real problem you're trying to solve.
```
created_at: 2025-03-21T21:34:06Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,114 @@
++++
+title = "WebSockets"
+time = 120
+emoji= "🔌"
+objectives = [
+    "Stream live updates from a server using WebSockets.",
+    "Discuss the trade-offs of using WebSockets or polling.",
+    "Describe properties of message formats (e.g. including a command name).",
+]
++++
+
+WebSockets are an API and protocol which allow creating a bi-directional communication channel between two programs.
+
+They are commonly used in websites to establish a channel so that a backend can send updates to a frontend.
+
+You can read [an introduction to WebSockets](https://docs.developer.tech.gov.sg/docs/data-engineering-initiative-playbook/Chapter5/Introduction_to_WebSockets), as well as roughly [what a client looks like](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_client_applications), and [what a server does](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers).
+
+On the server side, we will be using the [websocket npm package](https://www.npmjs.com/package/websocket) which lists a server example in its README.
+
+> [!TIP]
+>
+> This sprint, you will need to submit _both_ a copy of your code which supports polling, _and_ a copy which supports WebSockets.
+>
+> You probably want to make a copy of your polling code, and have two separate (similar) pages in your repo.
+
+On the backend, you can create a WebSocket server by adding this code:
+
+```js
+import { server as WebSocketServer } from "websocket";
+const server = http.createServer(app);
+const webSocketServer = new WebSocketServer({ httpServer: server });
+```
+
+You will then need to follow the example in the `websocket` npm package's documentation to have your server handle requests.
+
+On the client-side, you will need to make a new `WebSocket` connection to the server.
+
+Some things to think about when implementing WebSockets updates:
+
+### Learn new APIs in isolation
+
+It will be easier for you to learn a new API (like WebSockets) with a simple example.
+
+At the end, you will want your WebSocket to stream new messages from the server to the client, but maybe to explore WebSockets you want the server to always report the message "Hello" when it's connected to, so you can test things out more easily? Or even write a whole new website which _only_ makes a WebSocket connection and displays a message?
+
+Once you have an example WebSocket working, and understand how it works, it should be easier for you to apply that to the real problem you're trying to solve.
+
+### Think about the protocol you want
+
+WebSockets let you send arbitrary text (or binary) messages.
+
+In our quote server, we switched from our backend returning a pre-formatted string of a quote, to returning a JSON object so we could get the parts ourselves.
+
+Think about what structure would be useful for our client and our server to know about.
+
+If we're going to add more messages in the future (e.g. for "liking" a message), how will the receiver of the message know what kind of message the one it receives is?
+
+One thing we often do is wrap our message in an object, with a field specifically saying what the command is.
+
+e.g. instead of sending:
+```json
+{
+    "user": "Azin",
+    "message": "Hello!"
+}
+```
+
+we may send:
+
+```json
+{
+    "command": "send-message",
+    "message": {
+        "user": "Azin",
+        "message": "Hello!"
+    }
+}
+```
+
+This means that if we add new commands in the future, we don't need to change our existing code.
+
+### Think about timings
+
+When we first load a page, we need to get all of the messages that already exist.
+
path: common-content/en/module/decomposition/websockets/index.md
user: login: SallyMcGrath
body: Group some of these sentences into concept chunks please. Lines of paragraphs of one sentence each are more difficult to parse. 
created_at: 2025-03-21T21:35:30Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,114 @@
++++
+title = "WebSockets"
+time = 120
+emoji= "🔌"
+objectives = [
+    "Stream live updates from a server using WebSockets.",
+    "Discuss the trade-offs of using WebSockets or polling.",
+    "Describe properties of message formats (e.g. including a command name).",
+]
++++
+
+WebSockets are an API and protocol which allow creating a bi-directional communication channel between two programs.
+
+They are commonly used in websites to establish a channel so that a backend can send updates to a frontend.
+
+You can read [an introduction to WebSockets](https://docs.developer.tech.gov.sg/docs/data-engineering-initiative-playbook/Chapter5/Introduction_to_WebSockets), as well as roughly [what a client looks like](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_client_applications), and [what a server does](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers).
+
+On the server side, we will be using the [websocket npm package](https://www.npmjs.com/package/websocket) which lists a server example in its README.
+
+> [!TIP]
+>
+> This sprint, you will need to submit _both_ a copy of your code which supports polling, _and_ a copy which supports WebSockets.
+>
+> You probably want to make a copy of your polling code, and have two separate (similar) pages in your repo.
+
+On the backend, you can create a WebSocket server by adding this code:
+
+```js
+import { server as WebSocketServer } from "websocket";
+const server = http.createServer(app);
+const webSocketServer = new WebSocketServer({ httpServer: server });
+```
+
+You will then need to follow the example in the `websocket` npm package's documentation to have your server handle requests.
+
+On the client-side, you will need to make a new `WebSocket` connection to the server.
+
+Some things to think about when implementing WebSockets updates:
+
+### Learn new APIs in isolation
+
+It will be easier for you to learn a new API (like WebSockets) with a simple example.
+
+At the end, you will want your WebSocket to stream new messages from the server to the client, but maybe to explore WebSockets you want the server to always report the message "Hello" when it's connected to, so you can test things out more easily? Or even write a whole new website which _only_ makes a WebSocket connection and displays a message?
+
+Once you have an example WebSocket working, and understand how it works, it should be easier for you to apply that to the real problem you're trying to solve.
+
+### Think about the protocol you want
+
+WebSockets let you send arbitrary text (or binary) messages.
+
+In our quote server, we switched from our backend returning a pre-formatted string of a quote, to returning a JSON object so we could get the parts ourselves.
+
+Think about what structure would be useful for our client and our server to know about.
+
+If we're going to add more messages in the future (e.g. for "liking" a message), how will the receiver of the message know what kind of message the one it receives is?
+
+One thing we often do is wrap our message in an object, with a field specifically saying what the command is.
+
+e.g. instead of sending:
+```json
+{
+    "user": "Azin",
+    "message": "Hello!"
+}
+```
+
+we may send:
+
+```json
+{
+    "command": "send-message",
+    "message": {
+        "user": "Azin",
+        "message": "Hello!"
+    }
+}
+```
+
+This means that if we add new commands in the future, we don't need to change our existing code.
+
+### Think about timings
+
+When we first load a page, we need to get all of the messages that already exist.
+
+After that, we can ask to be notified of new messages.
+
+There are a few ways we could do that. An interesting question is what happens _between_ these events?
+
+Imagine we made an HTTP GET request to ask for all of the messages, then created a WebSocket to get new messages. What happens if someone sent a message between when we got our response, and when the WebSocket was connected? How can we make sure we don't miss any messages?
+
+Or imagine we made a WebSocket request, and expected to receive a list of all previous messages, and then to keep receiving updates. Does the server need to remember which messages have already been sent to each client?
+
+{{<note type="Exercise">}}
+Write down your strategy for how to make sure that after initially getting the existing messages, your client won't miss any new messages.
path: common-content/en/module/decomposition/websockets/index.md
user: login: SallyMcGrath
body: ```suggestion
How will you make sure that your client won't miss any new messages, after getting the initial messages? Write down your strategy.
```
created_at: 2025-03-21T21:36:33Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,114 @@
++++
+title = "WebSockets"
+time = 120
+emoji= "🔌"
+objectives = [
+    "Stream live updates from a server using WebSockets.",
+    "Discuss the trade-offs of using WebSockets or polling.",
+    "Describe properties of message formats (e.g. including a command name).",
+]
++++
+
+WebSockets are an API and protocol which allow creating a bi-directional communication channel between two programs.
+
+They are commonly used in websites to establish a channel so that a backend can send updates to a frontend.
+
+You can read [an introduction to WebSockets](https://docs.developer.tech.gov.sg/docs/data-engineering-initiative-playbook/Chapter5/Introduction_to_WebSockets), as well as roughly [what a client looks like](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_client_applications), and [what a server does](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers).
+
+On the server side, we will be using the [websocket npm package](https://www.npmjs.com/package/websocket) which lists a server example in its README.
+
+> [!TIP]
+>
+> This sprint, you will need to submit _both_ a copy of your code which supports polling, _and_ a copy which supports WebSockets.
+>
+> You probably want to make a copy of your polling code, and have two separate (similar) pages in your repo.
+
+On the backend, you can create a WebSocket server by adding this code:
+
+```js
+import { server as WebSocketServer } from "websocket";
+const server = http.createServer(app);
+const webSocketServer = new WebSocketServer({ httpServer: server });
+```
+
+You will then need to follow the example in the `websocket` npm package's documentation to have your server handle requests.
+
+On the client-side, you will need to make a new `WebSocket` connection to the server.
+
+Some things to think about when implementing WebSockets updates:
+
+### Learn new APIs in isolation
+
+It will be easier for you to learn a new API (like WebSockets) with a simple example.
+
+At the end, you will want your WebSocket to stream new messages from the server to the client, but maybe to explore WebSockets you want the server to always report the message "Hello" when it's connected to, so you can test things out more easily? Or even write a whole new website which _only_ makes a WebSocket connection and displays a message?
+
+Once you have an example WebSocket working, and understand how it works, it should be easier for you to apply that to the real problem you're trying to solve.
+
+### Think about the protocol you want
+
+WebSockets let you send arbitrary text (or binary) messages.
+
+In our quote server, we switched from our backend returning a pre-formatted string of a quote, to returning a JSON object so we could get the parts ourselves.
+
+Think about what structure would be useful for our client and our server to know about.
+
+If we're going to add more messages in the future (e.g. for "liking" a message), how will the receiver of the message know what kind of message the one it receives is?
+
+One thing we often do is wrap our message in an object, with a field specifically saying what the command is.
+
+e.g. instead of sending:
+```json
+{
+    "user": "Azin",
+    "message": "Hello!"
+}
+```
+
+we may send:
+
+```json
+{
+    "command": "send-message",
+    "message": {
+        "user": "Azin",
+        "message": "Hello!"
+    }
+}
+```
+
+This means that if we add new commands in the future, we don't need to change our existing code.
+
+### Think about timings
+
+When we first load a page, we need to get all of the messages that already exist.
+
+After that, we can ask to be notified of new messages.
+
+There are a few ways we could do that. An interesting question is what happens _between_ these events?
+
+Imagine we made an HTTP GET request to ask for all of the messages, then created a WebSocket to get new messages. What happens if someone sent a message between when we got our response, and when the WebSocket was connected? How can we make sure we don't miss any messages?
+
+Or imagine we made a WebSocket request, and expected to receive a list of all previous messages, and then to keep receiving updates. Does the server need to remember which messages have already been sent to each client?
+
+{{<note type="Exercise">}}
+Write down your strategy for how to make sure that after initially getting the existing messages, your client won't miss any new messages.
+{{</note>}}
+
+### Remember WebSockets are bi-directional
+
+Now, we're using a `POST` request to send a new message, and a `WebSocket` to stream receiving new messages.
+
path: common-content/en/module/decomposition/websockets/index.md
user: login: SallyMcGrath
body: chunk 
```suggestion
```
created_at: 2025-03-21T21:36:56Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,62 @@
++++
+title = "Adding like/dislike"
+time = 120
+emoji= "👍👎"
+objectives = [
+    "Identify what data needs to be stored and exchanged between a client and server.",
+    "Devise a scheme for differentiating messages with different meanings (e.g. a new message vs a new like).",
+    "Contrast giving updated values as absolute values or relative changes.",
+    "Implement an end-to-end feature involving data updates and reconciliation across a client and server.",
+]
++++
+
+The last requirement we have for our chat application is the ability to like/dislike a message (and see what messages have been liked/disliked).
+
+{{<note type="Exercise">}}
+Think about what information a client would need to provide to a server in order to like/dislike a message.
+
+Think about what information a server would need to provide to a client in order to display how many likes/dislikes a message has.
+
+Think about what information a server would need to provide to a client in order to _update_ how many likes/dislikes a message has.
+
+Write these things down.
+{{</note>}}
+
+### Identifiers
+
+One of the key new requirements to add liking/disliking a message is knowing _which_ message is being liked/disliked.
+
+When a client wants to like a message, it needs some way of saying _this_ is the message I want to like.
+
path: common-content/en/module/decomposition/adding-like-dislike/index.md
user: login: SallyMcGrath
body: ```suggestion
```
created_at: 2025-03-21T21:39:49Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,62 @@
++++
+title = "Adding like/dislike"
+time = 120
+emoji= "👍👎"
+objectives = [
+    "Identify what data needs to be stored and exchanged between a client and server.",
+    "Devise a scheme for differentiating messages with different meanings (e.g. a new message vs a new like).",
+    "Contrast giving updated values as absolute values or relative changes.",
+    "Implement an end-to-end feature involving data updates and reconciliation across a client and server.",
+]
++++
+
+The last requirement we have for our chat application is the ability to like/dislike a message (and see what messages have been liked/disliked).
+
+{{<note type="Exercise">}}
+Think about what information a client would need to provide to a server in order to like/dislike a message.
+
+Think about what information a server would need to provide to a client in order to display how many likes/dislikes a message has.
+
+Think about what information a server would need to provide to a client in order to _update_ how many likes/dislikes a message has.
+
+Write these things down.
+{{</note>}}
+
+### Identifiers
+
+One of the key new requirements to add liking/disliking a message is knowing _which_ message is being liked/disliked.
+
path: common-content/en/module/decomposition/adding-like-dislike/index.md
user: login: SallyMcGrath
body: ```suggestion
```
created_at: 2025-03-21T21:39:56Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,62 @@
++++
+title = "Adding like/dislike"
+time = 120
+emoji= "👍👎"
+objectives = [
+    "Identify what data needs to be stored and exchanged between a client and server.",
+    "Devise a scheme for differentiating messages with different meanings (e.g. a new message vs a new like).",
+    "Contrast giving updated values as absolute values or relative changes.",
+    "Implement an end-to-end feature involving data updates and reconciliation across a client and server.",
+]
++++
+
+The last requirement we have for our chat application is the ability to like/dislike a message (and see what messages have been liked/disliked).
+
+{{<note type="Exercise">}}
+Think about what information a client would need to provide to a server in order to like/dislike a message.
+
+Think about what information a server would need to provide to a client in order to display how many likes/dislikes a message has.
+
+Think about what information a server would need to provide to a client in order to _update_ how many likes/dislikes a message has.
+
+Write these things down.
+{{</note>}}
+
+### Identifiers
+
+One of the key new requirements to add liking/disliking a message is knowing _which_ message is being liked/disliked.
+
+When a client wants to like a message, it needs some way of saying _this_ is the message I want to like.
+
+This suggests we need a unique identifier for each message:
+* When the server tells a client about a message, it needs to tell it what the identifier is for that message.
+* When a client tells the server it wants to like a message, it needs to tell it the identifier for the message it wants to like.
+* When the server tells a client a message has been liked, it needs to tell the client which message was liked, and the client needs to know enough about that message to be able to update the UI.
+
+### Message formats
+
+Now that your server will be sending multiple kinds of updates ("Here's a new message", or "Here's an update to the number of likes of an existing message"), you'll need to make sure the client knows the difference between these messages. The client will need to know how to act when it receives each kind of message.
path: common-content/en/module/decomposition/adding-like-dislike/index.md
user: login: SallyMcGrath
body: ```suggestion
Your server is now sending multiple kinds of updates.  ("Here's a new message", or "Here's an update to the number of likes of an existing message". ) Make sure the client knows the difference between these messages. Your client will need to know how to act when it receives each kind of message.
```
created_at: 2025-03-21T21:41:11Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,62 @@
++++
+title = "Adding like/dislike"
+time = 120
+emoji= "👍👎"
+objectives = [
+    "Identify what data needs to be stored and exchanged between a client and server.",
+    "Devise a scheme for differentiating messages with different meanings (e.g. a new message vs a new like).",
+    "Contrast giving updated values as absolute values or relative changes.",
+    "Implement an end-to-end feature involving data updates and reconciliation across a client and server.",
+]
++++
+
+The last requirement we have for our chat application is the ability to like/dislike a message (and see what messages have been liked/disliked).
+
+{{<note type="Exercise">}}
+Think about what information a client would need to provide to a server in order to like/dislike a message.
+
+Think about what information a server would need to provide to a client in order to display how many likes/dislikes a message has.
+
+Think about what information a server would need to provide to a client in order to _update_ how many likes/dislikes a message has.
+
+Write these things down.
+{{</note>}}
+
+### Identifiers
+
+One of the key new requirements to add liking/disliking a message is knowing _which_ message is being liked/disliked.
+
+When a client wants to like a message, it needs some way of saying _this_ is the message I want to like.
+
+This suggests we need a unique identifier for each message:
+* When the server tells a client about a message, it needs to tell it what the identifier is for that message.
+* When a client tells the server it wants to like a message, it needs to tell it the identifier for the message it wants to like.
+* When the server tells a client a message has been liked, it needs to tell the client which message was liked, and the client needs to know enough about that message to be able to update the UI.
+
+### Message formats
+
+Now that your server will be sending multiple kinds of updates ("Here's a new message", or "Here's an update to the number of likes of an existing message"), you'll need to make sure the client knows the difference between these messages. The client will need to know how to act when it receives each kind of message.
+
+### Changes or absolutes?
+
+When new likes happen, a choice we need to make is whether the server should tell a client "this message was liked again" or should tell the client "this message now has 10 likes". Both of these can work.
+
+{{<note type="Exercise">}}
+Write down some advantages and disadvantages of a server -> client update being "+1 compared to before" or "now =10".
+
+Choose which approach you want to take.
+{{</note>}}
+
+{{<note type="Exercise">}}
+Implement liking and disliking messages.
+
+If a message has a non-zero number of likes or dislikes, the frontend needs to show this.
path: common-content/en/module/decomposition/adding-like-dislike/index.md
user: login: SallyMcGrath
body: ```suggestion
1. If a message has a non-zero number of likes or dislikes, the frontend needs to show this.
```
created_at: 2025-03-21T21:41:33Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,62 @@
++++
+title = "Adding like/dislike"
+time = 120
+emoji= "👍👎"
+objectives = [
+    "Identify what data needs to be stored and exchanged between a client and server.",
+    "Devise a scheme for differentiating messages with different meanings (e.g. a new message vs a new like).",
+    "Contrast giving updated values as absolute values or relative changes.",
+    "Implement an end-to-end feature involving data updates and reconciliation across a client and server.",
+]
++++
+
+The last requirement we have for our chat application is the ability to like/dislike a message (and see what messages have been liked/disliked).
+
+{{<note type="Exercise">}}
+Think about what information a client would need to provide to a server in order to like/dislike a message.
+
+Think about what information a server would need to provide to a client in order to display how many likes/dislikes a message has.
+
+Think about what information a server would need to provide to a client in order to _update_ how many likes/dislikes a message has.
+
+Write these things down.
+{{</note>}}
+
+### Identifiers
+
+One of the key new requirements to add liking/disliking a message is knowing _which_ message is being liked/disliked.
+
+When a client wants to like a message, it needs some way of saying _this_ is the message I want to like.
+
+This suggests we need a unique identifier for each message:
+* When the server tells a client about a message, it needs to tell it what the identifier is for that message.
+* When a client tells the server it wants to like a message, it needs to tell it the identifier for the message it wants to like.
+* When the server tells a client a message has been liked, it needs to tell the client which message was liked, and the client needs to know enough about that message to be able to update the UI.
+
+### Message formats
+
+Now that your server will be sending multiple kinds of updates ("Here's a new message", or "Here's an update to the number of likes of an existing message"), you'll need to make sure the client knows the difference between these messages. The client will need to know how to act when it receives each kind of message.
+
+### Changes or absolutes?
+
+When new likes happen, a choice we need to make is whether the server should tell a client "this message was liked again" or should tell the client "this message now has 10 likes". Both of these can work.
+
+{{<note type="Exercise">}}
+Write down some advantages and disadvantages of a server -> client update being "+1 compared to before" or "now =10".
+
+Choose which approach you want to take.
+{{</note>}}
+
+{{<note type="Exercise">}}
+Implement liking and disliking messages.
+
+If a message has a non-zero number of likes or dislikes, the frontend needs to show this.
+
+The frontend needs to expose some way for a user to like or dislike any message.
path: common-content/en/module/decomposition/adding-like-dislike/index.md
user: login: SallyMcGrath
body: ```suggestion
2. The frontend needs to expose some way for a user to like or dislike any message.
```
created_at: 2025-03-21T21:41:40Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,62 @@
++++
+title = "Adding like/dislike"
+time = 120
+emoji= "👍👎"
+objectives = [
+    "Identify what data needs to be stored and exchanged between a client and server.",
+    "Devise a scheme for differentiating messages with different meanings (e.g. a new message vs a new like).",
+    "Contrast giving updated values as absolute values or relative changes.",
+    "Implement an end-to-end feature involving data updates and reconciliation across a client and server.",
+]
++++
+
+The last requirement we have for our chat application is the ability to like/dislike a message (and see what messages have been liked/disliked).
+
+{{<note type="Exercise">}}
+Think about what information a client would need to provide to a server in order to like/dislike a message.
+
+Think about what information a server would need to provide to a client in order to display how many likes/dislikes a message has.
+
+Think about what information a server would need to provide to a client in order to _update_ how many likes/dislikes a message has.
+
+Write these things down.
+{{</note>}}
+
+### Identifiers
+
+One of the key new requirements to add liking/disliking a message is knowing _which_ message is being liked/disliked.
+
+When a client wants to like a message, it needs some way of saying _this_ is the message I want to like.
+
+This suggests we need a unique identifier for each message:
+* When the server tells a client about a message, it needs to tell it what the identifier is for that message.
+* When a client tells the server it wants to like a message, it needs to tell it the identifier for the message it wants to like.
+* When the server tells a client a message has been liked, it needs to tell the client which message was liked, and the client needs to know enough about that message to be able to update the UI.
+
+### Message formats
+
+Now that your server will be sending multiple kinds of updates ("Here's a new message", or "Here's an update to the number of likes of an existing message"), you'll need to make sure the client knows the difference between these messages. The client will need to know how to act when it receives each kind of message.
+
+### Changes or absolutes?
+
+When new likes happen, a choice we need to make is whether the server should tell a client "this message was liked again" or should tell the client "this message now has 10 likes". Both of these can work.
+
+{{<note type="Exercise">}}
+Write down some advantages and disadvantages of a server -> client update being "+1 compared to before" or "now =10".
+
+Choose which approach you want to take.
+{{</note>}}
+
+{{<note type="Exercise">}}
+Implement liking and disliking messages.
+
+If a message has a non-zero number of likes or dislikes, the frontend needs to show this.
+
+The frontend needs to expose some way for a user to like or dislike any message.
+
+When a user likes or dislikes a message, the frontend needs to tell the backend about this, and the backend needs to notify all clients of this.
path: common-content/en/module/decomposition/adding-like-dislike/index.md
user: login: SallyMcGrath
body: ```suggestion
3. When a user likes or dislikes a message, the frontend needs to tell the backend about this, and the backend needs to notify all clients of this.
```
created_at: 2025-03-21T21:41:48Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,62 @@
++++
+title = "Adding like/dislike"
+time = 120
+emoji= "👍👎"
+objectives = [
+    "Identify what data needs to be stored and exchanged between a client and server.",
+    "Devise a scheme for differentiating messages with different meanings (e.g. a new message vs a new like).",
+    "Contrast giving updated values as absolute values or relative changes.",
+    "Implement an end-to-end feature involving data updates and reconciliation across a client and server.",
+]
++++
+
+The last requirement we have for our chat application is the ability to like/dislike a message (and see what messages have been liked/disliked).
+
+{{<note type="Exercise">}}
+Think about what information a client would need to provide to a server in order to like/dislike a message.
+
+Think about what information a server would need to provide to a client in order to display how many likes/dislikes a message has.
+
+Think about what information a server would need to provide to a client in order to _update_ how many likes/dislikes a message has.
+
+Write these things down.
+{{</note>}}
+
+### Identifiers
+
+One of the key new requirements to add liking/disliking a message is knowing _which_ message is being liked/disliked.
+
+When a client wants to like a message, it needs some way of saying _this_ is the message I want to like.
+
+This suggests we need a unique identifier for each message:
+* When the server tells a client about a message, it needs to tell it what the identifier is for that message.
+* When a client tells the server it wants to like a message, it needs to tell it the identifier for the message it wants to like.
+* When the server tells a client a message has been liked, it needs to tell the client which message was liked, and the client needs to know enough about that message to be able to update the UI.
+
+### Message formats
+
+Now that your server will be sending multiple kinds of updates ("Here's a new message", or "Here's an update to the number of likes of an existing message"), you'll need to make sure the client knows the difference between these messages. The client will need to know how to act when it receives each kind of message.
+
+### Changes or absolutes?
+
+When new likes happen, a choice we need to make is whether the server should tell a client "this message was liked again" or should tell the client "this message now has 10 likes". Both of these can work.
+
+{{<note type="Exercise">}}
+Write down some advantages and disadvantages of a server -> client update being "+1 compared to before" or "now =10".
+
+Choose which approach you want to take.
+{{</note>}}
+
+{{<note type="Exercise">}}
+Implement liking and disliking messages.
+
+If a message has a non-zero number of likes or dislikes, the frontend needs to show this.
+
+The frontend needs to expose some way for a user to like or dislike any message.
+
+When a user likes or dislikes a message, the frontend needs to tell the backend about this, and the backend needs to notify all clients of this.
+
+When a frontend is notified by a backend about a new like or dislike, it needs to update the UI to show this.
path: common-content/en/module/decomposition/adding-like-dislike/index.md
user: login: SallyMcGrath
body: ```suggestion
4. When a frontend is notified by a backend about a new like or dislike, it needs to update the UI to show this.
```

helpful to number requirements, or offer in a checklist
created_at: 2025-03-21T21:42:07Z
author_association: MEMBER
diff_hunk: @@ -1,13 +1,95 @@
 +++
 title = "Extracting common functionality"
-headless = true
 time = 30
-emoji= "📖"
-[objectives]
-    1="Identify and explain the risks and costs of duplicate code"
-    2="Extract functionality used by multiple route handlers into a common function"
+emoji= "✂️"
+objectives = [
+    "Extract functionality used by multiple route handlers into a common function.",
+]
 +++
 
-### Extracting common functionality
+Having identified these three parts of our handlers, we can extract functions to make them more clear.
 
-TODO: Extract common code to determine whether the current user is an admin across multiple views
+One way we could do this is:
+
+```js {linenos=table}
+app.post("/message", (req, res) => {
+  parseRequestAsJsonObject(req, res, (req, res, body) => {
+    if (!ensureFields(body, res, ["user", "message"])) {
+      return;
+    }
+    receiveMessage(body.user, body.message);
+    res.send("ok");
+  });
+});
+
+app.post("/react", (req, res) => {
+  parseRequestAsJsonObject(req, res, (req, res, body) => {
+    if (!ensureFields(body, res, ["id", "reaction"])) {
+      return;
+    }
+    if (!(body.id in messageReactions)) {
+      res.status(404).send(`Got reaction to message id ${body.id} which doesn't exist.`);
+      return;
+    }
+    const reactionsForMessage = messageReactions[body.id];
+    if (!(body.reaction) in reactionsForMessage) {
+      res.status(400).send(`Reaction ${body.reaction} isn't allowed.`);
+      return;
+    }
+    reactionsForMessage[body.reaction]++;
+
+    res.send("ok");
+  });
+});
+
+const parseRequestAsJsonObject = (req, res, callback) => {
+  const bodyBytes = [];
+  req.on("data", chunk => bodyBytes.push(...chunk));
+  req.on("end", () => {
+    const bodyString = String.fromCharCode(...bodyBytes);
+    let body;
+    try {
+      body = JSON.parse(bodyString);
+    } catch (error) {
+      console.error(`Failed to parse body ${bodyString} as JSON: ${error}`);
+      res.status(400).send("Expected body to be JSON.");
+      return;
+    }
+    if (typeof body !== "object") {
+      console.error(`Got POST body which was not an object: ${bodyString}`);
+      res.status(400).send("Expected body to be a JSON object.");
+      return;
+    }
+    callback(req, res, body);
+  });
+}
+
+const ensureFields = (body, res, expectedFields) => {
+  const missingFields = [];
+  for (const expectedField of expectedFields) {
+    if (!(expectedField in body)) {
+      missingFields.push(expectedField);
+    }
+  }
+  if (missingFields.length > 0) {
+    const joinedExpectedFields = expectedFields.join(", ");
+    const joinedMissingFields = missingFields.join(", ");
+    console.error(`Failed to extract fields ${joinedMissingFields} from post body: ${bodyString}`);
+    res.status(400).send(`Expected body to be a JSON object containing keys ${joinedExpectedFields} but was missing ${joinedMissingFields}.`);
+    return false;
+  }
+  return true;
+};
+```
+
+## Why is this better?
+
+Before, the `/message` function body was 20 lines. 18 were just for processing the post body, and 2 were for the actual functionality. 10% of the function was for relevant functionality.
+
+Now, the function body is 7 lines. They say: "I expect the request to be a JSON object", "I expect these fields in the object", and then do the relevant functionality.
+
+We can see at a glance what the function does. And the common functionality has a clear name explaining what it's doing, rather than needing us to read 18 lines to work it out.
+
+We can also easily see the _difference_ between what the two endpoints do. They both expect the request to be a JSON object. They both expect some fields, but different fields. Then they do something different with them. This is much easier to see that before, where a subtle difference may have been hard to notice.
path: common-content/en/module/decomposition/extracting-common-functionality/index.md
user: login: SallyMcGrath
body: ```suggestion
We can also more easily see the _difference_ between what the two endpoints do. Each endpoint expects the request to be a JSON object. They both expect some fields, but different fields. Then they do something different with them. This is much easier to see than before, where a subtle difference may have been hard to notice.
```
created_at: 2025-03-21T21:43:58Z
author_association: MEMBER
diff_hunk: @@ -1,13 +1,95 @@
 +++
 title = "Extracting common functionality"
-headless = true
 time = 30
-emoji= "📖"
-[objectives]
-    1="Identify and explain the risks and costs of duplicate code"
-    2="Extract functionality used by multiple route handlers into a common function"
+emoji= "✂️"
+objectives = [
+    "Extract functionality used by multiple route handlers into a common function.",
+]
 +++
 
-### Extracting common functionality
+Having identified these three parts of our handlers, we can extract functions to make them more clear.
 
-TODO: Extract common code to determine whether the current user is an admin across multiple views
+One way we could do this is:
+
+```js {linenos=table}
+app.post("/message", (req, res) => {
+  parseRequestAsJsonObject(req, res, (req, res, body) => {
+    if (!ensureFields(body, res, ["user", "message"])) {
+      return;
+    }
+    receiveMessage(body.user, body.message);
+    res.send("ok");
+  });
+});
+
+app.post("/react", (req, res) => {
+  parseRequestAsJsonObject(req, res, (req, res, body) => {
+    if (!ensureFields(body, res, ["id", "reaction"])) {
+      return;
+    }
+    if (!(body.id in messageReactions)) {
+      res.status(404).send(`Got reaction to message id ${body.id} which doesn't exist.`);
+      return;
+    }
+    const reactionsForMessage = messageReactions[body.id];
+    if (!(body.reaction) in reactionsForMessage) {
+      res.status(400).send(`Reaction ${body.reaction} isn't allowed.`);
+      return;
+    }
+    reactionsForMessage[body.reaction]++;
+
+    res.send("ok");
+  });
+});
+
+const parseRequestAsJsonObject = (req, res, callback) => {
+  const bodyBytes = [];
+  req.on("data", chunk => bodyBytes.push(...chunk));
+  req.on("end", () => {
+    const bodyString = String.fromCharCode(...bodyBytes);
+    let body;
+    try {
+      body = JSON.parse(bodyString);
+    } catch (error) {
+      console.error(`Failed to parse body ${bodyString} as JSON: ${error}`);
+      res.status(400).send("Expected body to be JSON.");
+      return;
+    }
+    if (typeof body !== "object") {
+      console.error(`Got POST body which was not an object: ${bodyString}`);
+      res.status(400).send("Expected body to be a JSON object.");
+      return;
+    }
+    callback(req, res, body);
+  });
+}
+
+const ensureFields = (body, res, expectedFields) => {
+  const missingFields = [];
+  for (const expectedField of expectedFields) {
+    if (!(expectedField in body)) {
+      missingFields.push(expectedField);
+    }
+  }
+  if (missingFields.length > 0) {
+    const joinedExpectedFields = expectedFields.join(", ");
+    const joinedMissingFields = missingFields.join(", ");
+    console.error(`Failed to extract fields ${joinedMissingFields} from post body: ${bodyString}`);
+    res.status(400).send(`Expected body to be a JSON object containing keys ${joinedExpectedFields} but was missing ${joinedMissingFields}.`);
+    return false;
+  }
+  return true;
+};
+```
+
+## Why is this better?
+
+Before, the `/message` function body was 20 lines. 18 were just for processing the post body, and 2 were for the actual functionality. 10% of the function was for relevant functionality.
+
+Now, the function body is 7 lines. They say: "I expect the request to be a JSON object", "I expect these fields in the object", and then do the relevant functionality.
+
+We can see at a glance what the function does. And the common functionality has a clear name explaining what it's doing, rather than needing us to read 18 lines to work it out.
+
+We can also easily see the _difference_ between what the two endpoints do. They both expect the request to be a JSON object. They both expect some fields, but different fields. Then they do something different with them. This is much easier to see that before, where a subtle difference may have been hard to notice.
+
+We can also ignore the details of the helper functions if we don't need to know them. If we don't care exactly how the body is parsed as JSON, we can just trust that the function does it somehow. If we _do_ need to know, we can look into the function. If we need to change or fix something, we only need to do it in one place.
path: common-content/en/module/decomposition/extracting-common-functionality/index.md
user: login: SallyMcGrath
body: In the unfollow task in legacy code, I wanted to call back to both extract and identify in this sprint. 

I've put in the hooks, so let's try to remember when this is deployed!
created_at: 2025-03-21T21:45:23Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,266 @@
++++
+title = "Identifying common functionality"
+headless = true
+time = 30
+emoji= "🔎"
+objectives = [
+    "Identify and explain the risks and costs of duplicate code.",
+    "Identify which code is equivalent, equivalent with parameters, or unique.",
+]
++++
+
+Often in different HTTP handlers we do the same thing. This happens because we use similar patterns. For instance, we may expect all of our request bodies to contain JSON objects. Or we may expect all requests from signed in users to have an `Authorization` header in a certain format.
+
+For instance, an implementation of "receiving messages" and "receiving reactions to messages" may look like this:
+
+```js {linenos=table}
+app.post("/message", (req, res) => {
+  const bodyBytes = [];
+  req.on("data", chunk => bodyBytes.push(...chunk));
+  req.on("end", () => {
+    const bodyString = String.fromCharCode(...bodyBytes);
+    let body;
+    try {
+      body = JSON.parse(bodyString);
+    } catch (error) {
+      console.error(`Failed to parse body ${bodyString} as JSON: ${error}`);
+      res.status(400).send("Expected body to be JSON.");
+      return;
+    }
+    if (typeof body !== "object" || !("user" in body) || !("message" in body)) {
+      console.error(`Failed to extract user and message from post body: ${bodyString}`);
+      res.status(400).send("Expected body to be a JSON object containing keys user and message.");
+      return;
+    }
+    receiveMessage(body.user, body.message);
+    res.send("ok");
+  });
+});
+
+app.post("/react", (req, res) => {
+  const bodyBytes = [];
+  req.on("data", chunk => bodyBytes.push(...chunk));
+  req.on("end", () => {
+    const bodyString = String.fromCharCode(...bodyBytes);
+    let body;
+    try {
+      body = JSON.parse(bodyString);
+    } catch (error) {
+      console.error(`Failed to parse body ${bodyString} as JSON: ${error}`);
+      res.status(400).send("Expected body to be JSON.");
+      return;
+    }
+    if (typeof body !== "object" || !("id" in body) || !("reaction" in body)) {
+      console.error(`Failed to extract id and reaction from post body: ${bodyString}`);
+      res.status(400).send("Expected body to be a JSON object containing keys id and reaction.");
+      return;
+    }
+    if (!(body.id in messageReactions)) {
+      res.status(404).send(`Got reaction to message id ${body.id} which doesn't exist.`);
+      return;
+    }
+    const reactionsForMessage = messageReactions[body.id];
+    if (!(body.reaction) in reactionsForMessage) {
+      res.status(400).send(`Reaction ${body.reaction} isn't allowed.`);
+      return;
+    }
+    reactionsForMessage[body.reaction]++;
+
+    res.send("ok");
+  });
+});
+```
+
+(It's ok if your implementation looked different, but we're going to talk about this example).
+
+Both of these POST handlers mostly contain code for parsing a response body as JSON.
+
+This causes a few problems:
+1. Imagine we found a bug in our parsing - we'd need to fix that bug in multiple places.
+1. It's really hard to tell, at a glance, what each handler is actually doing.
+
+Let's try to work out what is specific to each handler, and what is more general shared behaviour.
+
+## Specific vs shared behaviours
+
+Both handlers accumulate chunks of the request as a string, then parse them as a JSON object, returning an error if this failed:
+
+```js {linenos=table,hl_lines=["2-13", "25-36"]}
+app.post("/message", (req, res) => {
+  const bodyBytes = [];
+  req.on("data", chunk => bodyBytes.push(...chunk));
+  req.on("end", () => {
+    const bodyString = String.fromCharCode(...bodyBytes);
+    let body;
+    try {
+      body = JSON.parse(bodyString);
+    } catch (error) {
+      console.error(`Failed to parse body ${bodyString} as JSON: ${error}`);
+      res.status(400).send("Expected body to be JSON.");
+      return;
+    }
+    if (typeof body !== "object" || !("user" in body) || !("message" in body)) {
+      console.error(`Failed to extract user and message from post body: ${bodyString}`);
+      res.status(400).send("Expected body to be a JSON object containing keys user and message.");
+      return;
+    }
+    receiveMessage(body.user, body.message);
+    res.send("ok");
+  });
+});
+
+app.post("/react", (req, res) => {
+  const bodyBytes = [];
+  req.on("data", chunk => bodyBytes.push(...chunk));
+  req.on("end", () => {
+    const bodyString = String.fromCharCode(...bodyBytes);
+    let body;
+    try {
+      body = JSON.parse(bodyString);
+    } catch (error) {
+      console.error(`Failed to parse body ${bodyString} as JSON: ${error}`);
+      res.status(400).send("Expected body to be JSON.");
+      return;
+    }
+    if (typeof body !== "object" || !("id" in body) || !("reaction" in body)) {
+      console.error(`Failed to extract id and reaction from post body: ${bodyString}`);
+      res.status(400).send("Expected body to be a JSON object containing keys id and reaction.");
+      return;
+    }
+    if (!(body.id in messageReactions)) {
+      res.status(404).send(`Got reaction to message id ${body.id} which doesn't exist.`);
+      return;
+    }
+    const reactionsForMessage = messageReactions[body.id];
+    if (!(body.reaction) in reactionsForMessage) {
+      res.status(400).send(`Reaction ${body.reaction} isn't allowed.`);
+      return;
+    }
+    reactionsForMessage[body.reaction]++;
+
+    res.send("ok");
+  });
+});
+```
+
+Both implementations check that the object contains certain keys, but they each check for different specific keys. These are doing the same thing, but with different parameters:
+
+```js {linenos=table,hl_lines=["14-18", "37-41"]}
+app.post("/message", (req, res) => {
+  const bodyBytes = [];
+  req.on("data", chunk => bodyBytes.push(...chunk));
+  req.on("end", () => {
+    const bodyString = String.fromCharCode(...bodyBytes);
+    let body;
+    try {
+      body = JSON.parse(bodyString);
+    } catch (error) {
+      console.error(`Failed to parse body ${bodyString} as JSON: ${error}`);
+      res.status(400).send("Expected body to be JSON.");
+      return;
+    }
+    if (typeof body !== "object" || !("user" in body) || !("message" in body)) {
+      console.error(`Failed to extract user and message from post body: ${bodyString}`);
+      res.status(400).send("Expected body to be a JSON object containing keys user and message.");
+      return;
+    }
+    receiveMessage(body.user, body.message);
+    res.send("ok");
+  });
+});
+
+app.post("/react", (req, res) => {
+  const bodyBytes = [];
+  req.on("data", chunk => bodyBytes.push(...chunk));
+  req.on("end", () => {
+    const bodyString = String.fromCharCode(...bodyBytes);
+    let body;
+    try {
+      body = JSON.parse(bodyString);
+    } catch (error) {
+      console.error(`Failed to parse body ${bodyString} as JSON: ${error}`);
+      res.status(400).send("Expected body to be JSON.");
+      return;
+    }
+    if (typeof body !== "object" || !("id" in body) || !("reaction" in body)) {
+      console.error(`Failed to extract id and reaction from post body: ${bodyString}`);
+      res.status(400).send("Expected body to be a JSON object containing keys id and reaction.");
+      return;
+    }
+    if (!(body.id in messageReactions)) {
+      res.status(404).send(`Got reaction to message id ${body.id} which doesn't exist.`);
+      return;
+    }
+    const reactionsForMessage = messageReactions[body.id];
+    if (!(body.reaction) in reactionsForMessage) {
+      res.status(400).send(`Reaction ${body.reaction} isn't allowed.`);
+      return;
+    }
+    reactionsForMessage[body.reaction]++;
+
+    res.send("ok");
+  });
+});
+```
+
+Then each implementation does something quite different - this is very specific to what the endpoint is for:
path: common-content/en/module/decomposition/identifying-common-functionality/index.md
user: login: SallyMcGrath
body: ```suggestion
Then each implementation does something quite different. This highlighted code is very specific to what the endpoint is for:
```
created_at: 2025-03-21T21:49:58Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,266 @@
++++
+title = "Identifying common functionality"
+headless = true
+time = 30
+emoji= "🔎"
+objectives = [
+    "Identify and explain the risks and costs of duplicate code.",
+    "Identify which code is equivalent, equivalent with parameters, or unique.",
+]
++++
+
+Often in different HTTP handlers we do the same thing. This happens because we use similar patterns. For instance, we may expect all of our request bodies to contain JSON objects. Or we may expect all requests from signed in users to have an `Authorization` header in a certain format.
+
+For instance, an implementation of "receiving messages" and "receiving reactions to messages" may look like this:
+
+```js {linenos=table}
+app.post("/message", (req, res) => {
+  const bodyBytes = [];
+  req.on("data", chunk => bodyBytes.push(...chunk));
+  req.on("end", () => {
+    const bodyString = String.fromCharCode(...bodyBytes);
+    let body;
+    try {
+      body = JSON.parse(bodyString);
+    } catch (error) {
+      console.error(`Failed to parse body ${bodyString} as JSON: ${error}`);
+      res.status(400).send("Expected body to be JSON.");
+      return;
+    }
+    if (typeof body !== "object" || !("user" in body) || !("message" in body)) {
+      console.error(`Failed to extract user and message from post body: ${bodyString}`);
+      res.status(400).send("Expected body to be a JSON object containing keys user and message.");
+      return;
+    }
+    receiveMessage(body.user, body.message);
+    res.send("ok");
+  });
+});
+
+app.post("/react", (req, res) => {
+  const bodyBytes = [];
+  req.on("data", chunk => bodyBytes.push(...chunk));
+  req.on("end", () => {
+    const bodyString = String.fromCharCode(...bodyBytes);
+    let body;
+    try {
+      body = JSON.parse(bodyString);
+    } catch (error) {
+      console.error(`Failed to parse body ${bodyString} as JSON: ${error}`);
+      res.status(400).send("Expected body to be JSON.");
+      return;
+    }
+    if (typeof body !== "object" || !("id" in body) || !("reaction" in body)) {
+      console.error(`Failed to extract id and reaction from post body: ${bodyString}`);
+      res.status(400).send("Expected body to be a JSON object containing keys id and reaction.");
+      return;
+    }
+    if (!(body.id in messageReactions)) {
+      res.status(404).send(`Got reaction to message id ${body.id} which doesn't exist.`);
+      return;
+    }
+    const reactionsForMessage = messageReactions[body.id];
+    if (!(body.reaction) in reactionsForMessage) {
+      res.status(400).send(`Reaction ${body.reaction} isn't allowed.`);
+      return;
+    }
+    reactionsForMessage[body.reaction]++;
+
+    res.send("ok");
+  });
+});
+```
+
+(It's ok if your implementation looked different, but we're going to talk about this example).
+
+Both of these POST handlers mostly contain code for parsing a response body as JSON.
+
+This causes a few problems:
+1. Imagine we found a bug in our parsing - we'd need to fix that bug in multiple places.
+1. It's really hard to tell, at a glance, what each handler is actually doing.
+
+Let's try to work out what is specific to each handler, and what is more general shared behaviour.
+
+## Specific vs shared behaviours
+
+Both handlers accumulate chunks of the request as a string, then parse them as a JSON object, returning an error if this failed:
+
+```js {linenos=table,hl_lines=["2-13", "25-36"]}
+app.post("/message", (req, res) => {
+  const bodyBytes = [];
+  req.on("data", chunk => bodyBytes.push(...chunk));
+  req.on("end", () => {
+    const bodyString = String.fromCharCode(...bodyBytes);
+    let body;
+    try {
+      body = JSON.parse(bodyString);
+    } catch (error) {
+      console.error(`Failed to parse body ${bodyString} as JSON: ${error}`);
+      res.status(400).send("Expected body to be JSON.");
+      return;
+    }
+    if (typeof body !== "object" || !("user" in body) || !("message" in body)) {
+      console.error(`Failed to extract user and message from post body: ${bodyString}`);
+      res.status(400).send("Expected body to be a JSON object containing keys user and message.");
+      return;
+    }
+    receiveMessage(body.user, body.message);
+    res.send("ok");
+  });
+});
+
+app.post("/react", (req, res) => {
+  const bodyBytes = [];
+  req.on("data", chunk => bodyBytes.push(...chunk));
+  req.on("end", () => {
+    const bodyString = String.fromCharCode(...bodyBytes);
+    let body;
+    try {
+      body = JSON.parse(bodyString);
+    } catch (error) {
+      console.error(`Failed to parse body ${bodyString} as JSON: ${error}`);
+      res.status(400).send("Expected body to be JSON.");
+      return;
+    }
+    if (typeof body !== "object" || !("id" in body) || !("reaction" in body)) {
+      console.error(`Failed to extract id and reaction from post body: ${bodyString}`);
+      res.status(400).send("Expected body to be a JSON object containing keys id and reaction.");
+      return;
+    }
+    if (!(body.id in messageReactions)) {
+      res.status(404).send(`Got reaction to message id ${body.id} which doesn't exist.`);
+      return;
+    }
+    const reactionsForMessage = messageReactions[body.id];
+    if (!(body.reaction) in reactionsForMessage) {
+      res.status(400).send(`Reaction ${body.reaction} isn't allowed.`);
+      return;
+    }
+    reactionsForMessage[body.reaction]++;
+
+    res.send("ok");
+  });
+});
+```
+
+Both implementations check that the object contains certain keys, but they each check for different specific keys. These are doing the same thing, but with different parameters:
+
+```js {linenos=table,hl_lines=["14-18", "37-41"]}
+app.post("/message", (req, res) => {
+  const bodyBytes = [];
+  req.on("data", chunk => bodyBytes.push(...chunk));
+  req.on("end", () => {
+    const bodyString = String.fromCharCode(...bodyBytes);
+    let body;
+    try {
+      body = JSON.parse(bodyString);
+    } catch (error) {
+      console.error(`Failed to parse body ${bodyString} as JSON: ${error}`);
+      res.status(400).send("Expected body to be JSON.");
+      return;
+    }
+    if (typeof body !== "object" || !("user" in body) || !("message" in body)) {
+      console.error(`Failed to extract user and message from post body: ${bodyString}`);
+      res.status(400).send("Expected body to be a JSON object containing keys user and message.");
+      return;
+    }
+    receiveMessage(body.user, body.message);
+    res.send("ok");
+  });
+});
+
+app.post("/react", (req, res) => {
+  const bodyBytes = [];
+  req.on("data", chunk => bodyBytes.push(...chunk));
+  req.on("end", () => {
+    const bodyString = String.fromCharCode(...bodyBytes);
+    let body;
+    try {
+      body = JSON.parse(bodyString);
+    } catch (error) {
+      console.error(`Failed to parse body ${bodyString} as JSON: ${error}`);
+      res.status(400).send("Expected body to be JSON.");
+      return;
+    }
+    if (typeof body !== "object" || !("id" in body) || !("reaction" in body)) {
+      console.error(`Failed to extract id and reaction from post body: ${bodyString}`);
+      res.status(400).send("Expected body to be a JSON object containing keys id and reaction.");
+      return;
+    }
+    if (!(body.id in messageReactions)) {
+      res.status(404).send(`Got reaction to message id ${body.id} which doesn't exist.`);
+      return;
+    }
+    const reactionsForMessage = messageReactions[body.id];
+    if (!(body.reaction) in reactionsForMessage) {
+      res.status(400).send(`Reaction ${body.reaction} isn't allowed.`);
+      return;
+    }
+    reactionsForMessage[body.reaction]++;
+
+    res.send("ok");
+  });
+});
+```
+
+Then each implementation does something quite different - this is very specific to what the endpoint is for:
+
+```js {linenos=table,hl_lines=["19-20", "42-53"]}
+app.post("/message", (req, res) => {
+  const bodyBytes = [];
+  req.on("data", chunk => bodyBytes.push(...chunk));
+  req.on("end", () => {
+    const bodyString = String.fromCharCode(...bodyBytes);
+    let body;
+    try {
+      body = JSON.parse(bodyString);
+    } catch (error) {
+      console.error(`Failed to parse body ${bodyString} as JSON: ${error}`);
+      res.status(400).send("Expected body to be JSON.");
+      return;
+    }
+    if (typeof body !== "object" || !("user" in body) || !("message" in body)) {
+      console.error(`Failed to extract user and message from post body: ${bodyString}`);
+      res.status(400).send("Expected body to be a JSON object containing keys user and message.");
+      return;
+    }
+    receiveMessage(body.user, body.message);
+    res.send("ok");
+  });
+});
+
+app.post("/react", (req, res) => {
+  const bodyBytes = [];
+  req.on("data", chunk => bodyBytes.push(...chunk));
+  req.on("end", () => {
+    const bodyString = String.fromCharCode(...bodyBytes);
+    let body;
+    try {
+      body = JSON.parse(bodyString);
+    } catch (error) {
+      console.error(`Failed to parse body ${bodyString} as JSON: ${error}`);
+      res.status(400).send("Expected body to be JSON.");
+      return;
+    }
+    if (typeof body !== "object" || !("id" in body) || !("reaction" in body)) {
+      console.error(`Failed to extract id and reaction from post body: ${bodyString}`);
+      res.status(400).send("Expected body to be a JSON object containing keys id and reaction.");
+      return;
+    }
+    if (!(body.id in messageReactions)) {
+      res.status(404).send(`Got reaction to message id ${body.id} which doesn't exist.`);
+      return;
+    }
+    const reactionsForMessage = messageReactions[body.id];
+    if (!(body.reaction) in reactionsForMessage) {
+      res.status(400).send(`Reaction ${body.reaction} isn't allowed.`);
+      return;
+    }
+    reactionsForMessage[body.reaction]++;
+
+    res.send("ok");
+  });
+});
+```
+
+Ideally, most of the code in these functions would be specific to the behaviour of the handler, and it would be easy to see what they do from reading them.
path: common-content/en/module/decomposition/identifying-common-functionality/index.md
user: login: SallyMcGrath
body: ```suggestion
Ideally, most of the code in these functions would be specific to the behaviour of the handler. It would be easy to see what each function does from reading them.
```
created_at: 2025-03-21T21:50:47Z
author_association: MEMBER
diff_hunk: @@ -1,12 +1,64 @@
 +++
 title = "Extracting a middleware"
-headless = true
-time = 30
-emoji= "📖"
-[objectives]
-    1="Identify and explain the risks and costs of manually calling code (e.g. authentication validators/extractors) from multiple route handlers"
-    2="Explain the middleware pattern"
-    3="Implement and wire up a middleware"
+time = 105
+emoji= "⛓️"
+objectives = [
+    "Identify and explain the risks and costs of manually calling code (e.g. authentication validators/extractors) from multiple route handlers.",
+    "Explain the middleware pattern.",
+    "Implement and wire up a middleware with Express.",
+]
 +++
 
-### Extracting a middleware
+It is very common that in an application there are operations we want to perform for most or all requests.
+
+For instance, we often want to check authentication details for _every_ request to an endpoint which requires authentication.
+
path: common-content/en/module/decomposition/extracting-a-middleware/index.md
user: login: SallyMcGrath
body: ```suggestion
```
created_at: 2025-03-21T21:58:35Z
author_association: MEMBER
diff_hunk: @@ -1,12 +1,64 @@
 +++
 title = "Extracting a middleware"
-headless = true
-time = 30
-emoji= "📖"
-[objectives]
-    1="Identify and explain the risks and costs of manually calling code (e.g. authentication validators/extractors) from multiple route handlers"
-    2="Explain the middleware pattern"
-    3="Implement and wire up a middleware"
+time = 105
+emoji= "⛓️"
+objectives = [
+    "Identify and explain the risks and costs of manually calling code (e.g. authentication validators/extractors) from multiple route handlers.",
+    "Explain the middleware pattern.",
+    "Implement and wire up a middleware with Express.",
+]
 +++
 
-### Extracting a middleware
+It is very common that in an application there are operations we want to perform for most or all requests.
+
+For instance, we often want to check authentication details for _every_ request to an endpoint which requires authentication.
+
+And typically we use the same format for all POST request bodies to an application (often JSON, but some applications use other formats) - we want to parse _all_ POST request bodies in that format.
+
+It can be annoying to have to write the same code (like calling `parseRequestAsJsonObject`) in every handler. I can also be dangerous to require doing so: If we forget to call a function to check a user is logged in in one endpoint, that may be a big security problem.
path: common-content/en/module/decomposition/extracting-a-middleware/index.md
user: login: SallyMcGrath
body: ```suggestion
It can be annoying to have to write the same code (like calling `parseRequestAsJsonObject`) in every handler. It can also be **dangerous** to require doing so: If we forget to call a function to check a user is logged in in one endpoint, that may be a big security problem.
```

You could also break it out into a warning

> [!WARNING]
> If we forget to call a function to check a user is logged in in one endpoint, that may be a big security problem.
created_at: 2025-03-21T21:59:04Z
author_association: MEMBER
diff_hunk: @@ -1,12 +1,64 @@
 +++
 title = "Extracting a middleware"
-headless = true
-time = 30
-emoji= "📖"
-[objectives]
-    1="Identify and explain the risks and costs of manually calling code (e.g. authentication validators/extractors) from multiple route handlers"
-    2="Explain the middleware pattern"
-    3="Implement and wire up a middleware"
+time = 105
+emoji= "⛓️"
+objectives = [
+    "Identify and explain the risks and costs of manually calling code (e.g. authentication validators/extractors) from multiple route handlers.",
+    "Explain the middleware pattern.",
+    "Implement and wire up a middleware with Express.",
+]
 +++
 
-### Extracting a middleware
+It is very common that in an application there are operations we want to perform for most or all requests.
+
+For instance, we often want to check authentication details for _every_ request to an endpoint which requires authentication.
+
+And typically we use the same format for all POST request bodies to an application (often JSON, but some applications use other formats) - we want to parse _all_ POST request bodies in that format.
+
+It can be annoying to have to write the same code (like calling `parseRequestAsJsonObject`) in every handler. I can also be dangerous to require doing so: If we forget to call a function to check a user is logged in in one endpoint, that may be a big security problem.
+
+One strategy to improve this is to use a {{<tooltip title="Middleware" text="middleware">}}A middleware is a piece of code which is called before route handlers.{{</tooltip>}}. A middleware may process the request, do extra checks (e.g. check an authorization token), or attach extra data (e.g. parsing a POST body as JSON and adding the fields to the request object). It can choose to respond to the request itself, or allow the route handler to do so.
+
+{{<note type="Reading">}}
+Read the [Express documentation on writing middleware](https://expressjs.com/en/guide/writing-middleware.html).
+{{</note>}}
+
+{{<note type="Exercise">}}
+Write an Express application. You must write two separate middlewares.
path: common-content/en/module/decomposition/extracting-a-middleware/index.md
user: login: SallyMcGrath
body: Keep prompting them to keep it small and focused

```suggestion
Write a tiny Express application. You must write two separate middlewares.
```
created_at: 2025-03-21T22:00:49Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,114 @@
++++
+title = "WebSockets"
+time = 120
+emoji= "🔌"
+objectives = [
+    "Stream live updates from a server using WebSockets.",
+    "Discuss the trade-offs of using WebSockets or polling.",
+    "Describe properties of message formats (e.g. including a command name).",
+]
++++
+
+WebSockets are an API and protocol which allow creating a bi-directional communication channel between two programs.
+
+They are commonly used in websites to establish a channel so that a backend can send updates to a frontend.
+
+You can read [an introduction to WebSockets](https://docs.developer.tech.gov.sg/docs/data-engineering-initiative-playbook/Chapter5/Introduction_to_WebSockets), as well as roughly [what a client looks like](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_client_applications), and [what a server does](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers).
+
+On the server side, we will be using the [websocket npm package](https://www.npmjs.com/package/websocket) which lists a server example in its README.
+
+> [!TIP]
+>
+> This sprint, you will need to submit _both_ a copy of your code which supports polling, _and_ a copy which supports WebSockets.
path: common-content/en/module/decomposition/websockets/index.md
user: login: illicitonion
body: I think that's realistically a lot more hassle to maintain than two copies of the code... I've added a note to suggest sharing code between them where possible, WDYT?
created_at: 2025-03-25T12:12:01Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,114 @@
++++
+title = "WebSockets"
+time = 120
+emoji= "🔌"
+objectives = [
+    "Stream live updates from a server using WebSockets.",
+    "Discuss the trade-offs of using WebSockets or polling.",
+    "Describe properties of message formats (e.g. including a command name).",
+]
++++
+
+WebSockets are an API and protocol which allow creating a bi-directional communication channel between two programs.
+
+They are commonly used in websites to establish a channel so that a backend can send updates to a frontend.
+
+You can read [an introduction to WebSockets](https://docs.developer.tech.gov.sg/docs/data-engineering-initiative-playbook/Chapter5/Introduction_to_WebSockets), as well as roughly [what a client looks like](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_client_applications), and [what a server does](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers).
+
+On the server side, we will be using the [websocket npm package](https://www.npmjs.com/package/websocket) which lists a server example in its README.
+
+> [!TIP]
+>
+> This sprint, you will need to submit _both_ a copy of your code which supports polling, _and_ a copy which supports WebSockets.
path: common-content/en/module/decomposition/websockets/index.md
user: login: SallyMcGrath
body: Yep - just checking tbh. We have trained them a bit to return to main and make a new branch for each feature, so it might be worth saying do it all in one this time.
created_at: 2025-03-25T13:26:08Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,114 @@
++++
+title = "WebSockets"
+time = 120
+emoji= "🔌"
+objectives = [
+    "Stream live updates from a server using WebSockets.",
+    "Discuss the trade-offs of using WebSockets or polling.",
+    "Describe properties of message formats (e.g. including a command name).",
+]
++++
+
+WebSockets are an API and protocol which allow creating a bi-directional communication channel between two programs.
+
+They are commonly used in websites to establish a channel so that a backend can send updates to a frontend.
+
+You can read [an introduction to WebSockets](https://docs.developer.tech.gov.sg/docs/data-engineering-initiative-playbook/Chapter5/Introduction_to_WebSockets), as well as roughly [what a client looks like](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_client_applications), and [what a server does](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers).
+
+On the server side, we will be using the [websocket npm package](https://www.npmjs.com/package/websocket) which lists a server example in its README.
+
+> [!TIP]
+>
+> This sprint, you will need to submit _both_ a copy of your code which supports polling, _and_ a copy which supports WebSockets.
path: common-content/en/module/decomposition/websockets/index.md
user: login: illicitonion
body: Done!
created_at: 2025-03-25T15:23:21Z
author_association: MEMBER

PR comments:
user: login: netlify[bot]
created_at: 2025-03-14T16:11:46Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *cyf-curriculum* canceled.


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 0d34a8e18d4212d4660639a3b35f7db61009755c |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/cyf-curriculum/deploys/67e2ca65d5bdce0008b94f50 |
author_association: NONE
user: login: netlify[bot]
created_at: 2025-03-14T16:11:50Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *cyf-sdc* ready!


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 0d34a8e18d4212d4660639a3b35f7db61009755c |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/cyf-sdc/deploys/67e2ca653fe42600088ac258 |
|<span aria-hidden="true">😎</span> Deploy Preview | [https://deploy-preview-1388--cyf-sdc.netlify.app](https://deploy-preview-1388--cyf-sdc.netlify.app) |
|<span aria-hidden="true">📱</span> Preview on mobile | <details><summary> Toggle QR Code... </summary><br /><br />![QR Code](https://app.netlify.com/qr-code/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2RlcGxveS1wcmV2aWV3LTEzODgtLWN5Zi1zZGMubmV0bGlmeS5hcHAifQ.IRXqyQz6uhjs7pE6_8RwmVApuoxhRgzUZrv7_o9QASM)<br /><br />_Use your smartphone camera to open QR code link._</details> |
---
<!-- [cyf-sdc Preview](https://deploy-preview-1388--cyf-sdc.netlify.app) -->
_To edit notification comments on pull requests, go to your [Netlify site configuration](https://app.netlify.com/sites/cyf-sdc/configuration/notifications#deploy-webhooks)._
author_association: NONE
user: login: netlify[bot]
created_at: 2025-03-14T16:11:58Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *cyf-common* ready!


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 0d34a8e18d4212d4660639a3b35f7db61009755c |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/cyf-common/deploys/67e2ca65c2148d0008670762 |
|<span aria-hidden="true">😎</span> Deploy Preview | [https://deploy-preview-1388--cyf-common.netlify.app](https://deploy-preview-1388--cyf-common.netlify.app) |
|<span aria-hidden="true">📱</span> Preview on mobile | <details><summary> Toggle QR Code... </summary><br /><br />![QR Code](https://app.netlify.com/qr-code/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2RlcGxveS1wcmV2aWV3LTEzODgtLWN5Zi1jb21tb24ubmV0bGlmeS5hcHAifQ.co9znk8D7fEqdEM_AWbzh1vMh3cVV_Yi2ojc7KwB-ZM)<br /><br />_Use your smartphone camera to open QR code link._</details> |
---
<!-- [cyf-common Preview](https://deploy-preview-1388--cyf-common.netlify.app) -->
_To edit notification comments on pull requests, go to your [Netlify site configuration](https://app.netlify.com/sites/cyf-common/configuration/notifications#deploy-webhooks)._
author_association: NONE
user: login: netlify[bot]
created_at: 2025-03-14T16:11:59Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *cyf-tracks* ready!


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 0d34a8e18d4212d4660639a3b35f7db61009755c |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/cyf-tracks/deploys/67e2ca65228c1f0008255b8b |
|<span aria-hidden="true">😎</span> Deploy Preview | [https://deploy-preview-1388--cyf-tracks.netlify.app](https://deploy-preview-1388--cyf-tracks.netlify.app) |
|<span aria-hidden="true">📱</span> Preview on mobile | <details><summary> Toggle QR Code... </summary><br /><br />![QR Code](https://app.netlify.com/qr-code/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2RlcGxveS1wcmV2aWV3LTEzODgtLWN5Zi10cmFja3MubmV0bGlmeS5hcHAifQ.6mUk-G6Kz_YiOr_DqXpCEXSYVat435vNn9yh6-C0wAE)<br /><br />_Use your smartphone camera to open QR code link._</details> |
---
<!-- [cyf-tracks Preview](https://deploy-preview-1388--cyf-tracks.netlify.app) -->
_To edit notification comments on pull requests, go to your [Netlify site configuration](https://app.netlify.com/sites/cyf-tracks/configuration/notifications#deploy-webhooks)._
author_association: NONE
user: login: netlify[bot]
created_at: 2025-03-14T16:11:59Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *cyf-programming* ready!


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 0d34a8e18d4212d4660639a3b35f7db61009755c |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/cyf-programming/deploys/67e2ca6578767b000766657d |
|<span aria-hidden="true">😎</span> Deploy Preview | [https://deploy-preview-1388--cyf-programming.netlify.app](https://deploy-preview-1388--cyf-programming.netlify.app) |
|<span aria-hidden="true">📱</span> Preview on mobile | <details><summary> Toggle QR Code... </summary><br /><br />![QR Code](https://app.netlify.com/qr-code/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2RlcGxveS1wcmV2aWV3LTEzODgtLWN5Zi1wcm9ncmFtbWluZy5uZXRsaWZ5LmFwcCJ9.7fhDTG2F64ws-e_41doPNKqDULb3vTwOe_L2d8tag3Q)<br /><br />_Use your smartphone camera to open QR code link._</details> |
|<span aria-hidden="true">![Lighthouse](https://app.netlify.com/public/external-usage/git-comments/lighthouse-sm.png)</span><br />Lighthouse | 1 paths audited<br />**Performance**: 61 (🔴 down 38 from production)<br />**Accessibility**: 100 (no change from production)<br />**Best Practices**: 92 (no change from production)<br />**SEO**: 90 (no change from production)<br />**PWA**: -<br />[View the detailed breakdown and full score reports](https://app.netlify.com/sites/cyf-programming/deploys/67e2ca6578767b000766657d) |
---
<!-- [cyf-programming Preview](https://deploy-preview-1388--cyf-programming.netlify.app) -->
_To edit notification comments on pull requests, go to your [Netlify site configuration](https://app.netlify.com/sites/cyf-programming/configuration/notifications#deploy-webhooks)._
author_association: NONE
user: login: netlify[bot]
created_at: 2025-03-14T16:12:12Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *cyf-itd* ready!


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 0d34a8e18d4212d4660639a3b35f7db61009755c |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/cyf-itd/deploys/67e2ca657b4cad0008061c3a |
|<span aria-hidden="true">😎</span> Deploy Preview | [https://deploy-preview-1388--cyf-itd.netlify.app](https://deploy-preview-1388--cyf-itd.netlify.app) |
|<span aria-hidden="true">📱</span> Preview on mobile | <details><summary> Toggle QR Code... </summary><br /><br />![QR Code](https://app.netlify.com/qr-code/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2RlcGxveS1wcmV2aWV3LTEzODgtLWN5Zi1pdGQubmV0bGlmeS5hcHAifQ.ogJcOBd6CEzFTAhaqaHdI8r8EwRrwTQYrtYUwJy3tqc)<br /><br />_Use your smartphone camera to open QR code link._</details> |
|<span aria-hidden="true">![Lighthouse](https://app.netlify.com/public/external-usage/git-comments/lighthouse-sm.png)</span><br />Lighthouse | 1 paths audited<br />**Performance**: 76 (🔴 down 20 from production)<br />**Accessibility**: 100 (no change from production)<br />**Best Practices**: 92 (no change from production)<br />**SEO**: 100 (no change from production)<br />**PWA**: -<br />[View the detailed breakdown and full score reports](https://app.netlify.com/sites/cyf-itd/deploys/67e2ca657b4cad0008061c3a) |
---
<!-- [cyf-itd Preview](https://deploy-preview-1388--cyf-itd.netlify.app) -->
_To edit notification comments on pull requests, go to your [Netlify site configuration](https://app.netlify.com/sites/cyf-itd/configuration/notifications#deploy-webhooks)._
author_association: NONE
user: login: netlify[bot]
created_at: 2025-03-14T16:13:32Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *cyf-launch* ready!


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 0d34a8e18d4212d4660639a3b35f7db61009755c |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/cyf-launch/deploys/67e2ca65089f8100080f3454 |
|<span aria-hidden="true">😎</span> Deploy Preview | [https://deploy-preview-1388--cyf-launch.netlify.app](https://deploy-preview-1388--cyf-launch.netlify.app) |
|<span aria-hidden="true">📱</span> Preview on mobile | <details><summary> Toggle QR Code... </summary><br /><br />![QR Code](https://app.netlify.com/qr-code/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2RlcGxveS1wcmV2aWV3LTEzODgtLWN5Zi1sYXVuY2gubmV0bGlmeS5hcHAifQ.hETbWBPmUP2v4yO7ZxaFKE8iBkaOAjtwqOFjTrgoyrE)<br /><br />_Use your smartphone camera to open QR code link._</details> |
---
<!-- [cyf-launch Preview](https://deploy-preview-1388--cyf-launch.netlify.app) -->
_To edit notification comments on pull requests, go to your [Netlify site configuration](https://app.netlify.com/sites/cyf-launch/configuration/notifications#deploy-webhooks)._
author_association: NONE
user: login: netlify[bot]
created_at: 2025-03-14T16:14:09Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *cyf-piscine* ready!


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 0d34a8e18d4212d4660639a3b35f7db61009755c |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/cyf-piscine/deploys/67e2ca65e8ab4e0008514664 |
|<span aria-hidden="true">😎</span> Deploy Preview | [https://deploy-preview-1388--cyf-piscine.netlify.app](https://deploy-preview-1388--cyf-piscine.netlify.app) |
|<span aria-hidden="true">📱</span> Preview on mobile | <details><summary> Toggle QR Code... </summary><br /><br />![QR Code](https://app.netlify.com/qr-code/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2RlcGxveS1wcmV2aWV3LTEzODgtLWN5Zi1waXNjaW5lLm5ldGxpZnkuYXBwIn0.ly44oXfLOIVEJ_ZheuBJcYiC_odr3qgUnIWim22sOGk)<br /><br />_Use your smartphone camera to open QR code link._</details> |
|<span aria-hidden="true">![Lighthouse](https://app.netlify.com/public/external-usage/git-comments/lighthouse-sm.png)</span><br />Lighthouse | 1 paths audited<br />**Performance**: 58 (🔴 down 27 from production)<br />**Accessibility**: 100 (no change from production)<br />**Best Practices**: 92 (no change from production)<br />**SEO**: 100 (no change from production)<br />**PWA**: -<br />[View the detailed breakdown and full score reports](https://app.netlify.com/sites/cyf-piscine/deploys/67e2ca65e8ab4e0008514664) |
---
<!-- [cyf-piscine Preview](https://deploy-preview-1388--cyf-piscine.netlify.app) -->
_To edit notification comments on pull requests, go to your [Netlify site configuration](https://app.netlify.com/sites/cyf-piscine/configuration/notifications#deploy-webhooks)._
author_association: NONE