-
-
Notifications
You must be signed in to change notification settings - Fork 56
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
SDC Decomposition Sprint 1 #1295
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,35 @@ | ||||||
+++ | ||||||
title = "Adding quotes" | ||||||
headless = true | ||||||
time = 60 | ||||||
facilitation = false | ||||||
emoji= "➕" | ||||||
hide_from_overview = true | ||||||
objectives = [ | ||||||
"POST data from a frontend to a backend in JSON format.", | ||||||
] | ||||||
+++ | ||||||
|
||||||
{{<note type="Exercise">}} | ||||||
Add a form to your frontend which allows users to add quotes to the backend's list of quotes. | ||||||
|
||||||
Note: Your backend expects the quotes to be submitted as JSON. This means you will need to use a `fetch` request from JavaScript to do the POSTing. | ||||||
|
||||||
You can't just use a `<form method="POST">` tag because that would post the information in a different format. (It would also redirect the user to a page which just says "ok" after submitting the quote, which isn't a great user experience!) | ||||||
{{</note>}} | ||||||
|
||||||
After a user tries to add a quote, if they successfully added the quote we should give the user some feedback so they know it was successful. | ||||||
|
||||||
If a user tried to add a quote and the `fetch` failed (perhaps because the backend wasn't running), or the response said there was an error, we should give the user some feedback so they know something went wrong (and maybe what they should do about it). | ||||||
|
||||||
Now that our backend allows users to post quotes, we may want to restrict what can be posted. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
For instance, we probably don't want to let people post quotes which are empty. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
You can stop hedging. We never want to publish raw user inputs. We can just say this. |
||||||
|
||||||
We may want to do some validation of the input our users give us. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
{{<note type="Think">}} | ||||||
Where do you think we want to do this validation? | ||||||
|
||||||
On the frontend? On the backend? Or both? | ||||||
{{</note>}} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,57 @@ | ||||||
+++ | ||||||
title = "Limitations of backends" | ||||||
headless = true | ||||||
time = 20 | ||||||
facilitation = false | ||||||
emoji= "📖" | ||||||
objectives = [ | ||||||
"Explain why a backend on its own doesn't provide reliable data persistence.", | ||||||
"Explain why we may prefer doing work in a frontend rather than backend to avoid latency.", | ||||||
] | ||||||
+++ | ||||||
|
||||||
We've already explored limitations of frontends. | ||||||
|
||||||
We know a backend is just a program that runs for a long time. | ||||||
|
||||||
### Lifetime | ||||||
|
||||||
A major limitation of backends is that "a long time" probably isn't forever. | ||||||
|
||||||
Sometimes we change the code of the backend. Or need restart the computer it's running on for an upgrade. Or its computer loses power and we need to start it again. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Suggest bringing in something from cyf+ eg computers actually exist in the world. |
||||||
|
||||||
When this happens, the program starts again. | ||||||
|
||||||
Think back to our quote server that allows users to POST new quotes. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't need every sentence as its own paragraph. Compress these a little please? |
||||||
|
||||||
If we had to stop the server and start it again, we would lose all of the quotes users had saved. | ||||||
|
||||||
They're just stored in a variable, and a variable only lasts while the program it's in is running. | ||||||
|
||||||
### Location | ||||||
|
||||||
Another major limitation of a backend is where the code runs. | ||||||
|
||||||
A backend's code runs on whatever server it's running on. | ||||||
|
||||||
In contrast, a web frontend's code runs in the user's web browser. | ||||||
|
||||||
#### Latency | ||||||
|
||||||
One problem here is latency. Depending on where the backend and the user are physically located, it may take anywhere between 1ms and 500ms for a request to go between them. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. lead with a concrete example. We have already talked about latency -- you can bring that definition in here and reuse it if you want. But make a clear bridge here to prior learning. |
||||||
|
||||||
If every time you clicked on something on a web page you needed to talk to the backend, you may need to wait half a second just for the request to travel to the server and for the response to travel back, ignoring how long it takes to actually process the request. This would be unusably slow for many applications. | ||||||
|
||||||
#### Context | ||||||
|
||||||
Because web frontends run in the user's web browser, they have easy access to lots of information about the user's computer. For instance, they know what language it's configured in, what time zone it's configured in, how big the browser window is, etc. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
If our frontend code were instead running in a backend, the browser may need to include all of this information in every request it makes, just in case the backend needs to know it. This has a couple of drawbacks: It makes the requests bigger (which makes them slower, and maybe cost more), and it ends up sharing lots of data with the server that it may not need, which may compromise the user's privacy. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another concrete analogy please. Tie it down to the world. What is this like, that they already know and understand? |
||||||
|
||||||
### Pull not push | ||||||
|
||||||
A backend lives at a well-known address - we know how to connect to it. A user's web browser does not. | ||||||
|
||||||
This means that a backend cannot try to open a connection to a user's web browser. The web browser needs to initiate the request, and then the backend can reply to the request. | ||||||
|
||||||
Once a web browser opens a request to a backend, there are some ways to keep a bi-directional communication channel open. But the very first request needs to come from the web browser. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
+++ | ||
title = "Backend statefulness" | ||
headless = true | ||
time = 30 | ||
facilitation = false | ||
emoji= "📖" | ||
objectives = [ | ||
"Identify whether some program is stateful or stateless.", | ||
] | ||
+++ | ||
|
||
Our example backend is **stateless**. If you make several requests to it, it will always do the same thing (even though doesn't always return exactly the same result!). | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. tooltip on stateless |
||
|
||
If you made a request from a different computer, or from different country, or on a different day, it would keep doing exactly the same thing. If you restarted the server, it would keep doing exactly the same thing. | ||
|
||
If our backend allowed users to add new quotes, it would start having _state_. It would need to remember what quotes had been added. It would be **stateful**. | ||
|
||
```js | ||
import express from "express"; | ||
|
||
const app = express(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Given that we haven't taught them express, do you think we should? I don't have. strong view on this just making sure you've thought about it. |
||
const port = 3000; | ||
|
||
const quotes = [ | ||
{ | ||
quote: "Either write something worth reading or do something worth writing.", | ||
author: "Benjamin Franklin", | ||
}, | ||
{ | ||
quote: "I should have been more kind.", | ||
author: "Clive James", | ||
}, | ||
]; | ||
|
||
function randomQuote() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. BTW you can define runkits with express using runkit codefence |
||
const index = Math.floor(Math.random() * quotes.length); | ||
return quotes[index]; | ||
} | ||
|
||
app.get("/", (req, res) => { | ||
const quote = randomQuote(); | ||
res.send(`"${quote.quote}" -${quote.author}`); | ||
}); | ||
|
||
app.post("/", (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" || !("quote" in body) || !("author" in body)) { | ||
console.error(`Failed to extract quote and author from post body: ${bodyString}`); | ||
res.status(400).send("Expected body to be a JSON object containing keys quote and author."); | ||
return; | ||
} | ||
quotes.push({ | ||
quote: body.quote, | ||
author: body.author, | ||
}); | ||
res.send("ok"); | ||
}); | ||
}); | ||
|
||
app.listen(port, () => { | ||
console.error(`Quote server listening on port ${port}`); | ||
}); | ||
``` | ||
|
||
Here we have added a new request handler. If someone makes a POST request to the path `/`, we try to interpret the body they posted as a JSON object. | ||
|
||
If we can find a quote and author in it, we will store it in our list of quotes, and start serving it up to future requests. | ||
|
||
If we can't process the request, we return an error describing what went wrong. | ||
|
||
The details of exactly how we understand the request aren't important. The important thing is that we _are_ taking information from the request, and are then modifying the `quotes` array. | ||
|
||
> [!NOTE] | ||
> **State** is a general term that is used for related but different things in different contexts. | ||
> | ||
> State almost always refers to information that we store, which may change. | ||
|
||
Because we're _modifying_ the `quotes` array, it is now state. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
+++ | ||
title = "Data validation" | ||
headless = true | ||
time = 60 | ||
facilitation = false | ||
emoji= "🔎" | ||
hidden_from_overview = true | ||
objectives = [ | ||
"Explain the trade-offs of doing validation on the frontend or backend.", | ||
] | ||
+++ | ||
|
||
If we only do the validation on the backend, the user won't know anything is wrong until they submit the form. | ||
|
||
If we only do validation on the frontend, it would be possible for users to add invalid quotes by using `curl` themselves, or building their own frontend. | ||
|
||
So we normally do validation _twice_ - once in the frontend to give fast feedback (e.g. by adding the `required` attribute to an `<input>` tag), and once in the backend (to make sure no one can sneak bad data to us). | ||
|
||
{{<note type="Exercise">}} | ||
Add validation that authors and quotes are both non-empty, to both your frontend, and your backend. | ||
|
||
Make sure when someone tries to post an empty quote, make sure they quickly know what's wrong. | ||
|
||
Make sure if someone tries to use `curl` to post an empty quote, it doesn't get saved either. | ||
|
||
If the backend rejects a request from the frontend, we should show the user why. | ||
{{</note>}} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,39 @@ | ||||||
+++ | ||||||
title = "Deploy a frontend and backend" | ||||||
headless = true | ||||||
time = 60 | ||||||
facilitation = false | ||||||
emoji= "📖" | ||||||
objectives = [ | ||||||
"Deploy a frontend and backend", | ||||||
"Configure a frontend to talk to a specific deployed backend", | ||||||
] | ||||||
+++ | ||||||
|
||||||
Websites tend to only be useful if they're running somewhere that people can access them. | ||||||
|
||||||
We need to deploy our frontend and our backend on the internet so that people can use them. | ||||||
|
||||||
And they need to know how to talk to each other. In your frontend you probably hard-coded your `fetch` to fetch from `http://127.0.0.1:3000`. | ||||||
|
||||||
First let's deploy our backend somewhere. Then when we know its address, we can update our frontend to talk to our deployed backend, and then deploy it too. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I want some pictures. Or some diagrams? |
||||||
|
||||||
{{<note type="Exercise">}} | ||||||
We also need to store our frontend and backend somewhere to deploy them from: | ||||||
1. Make a new Git repository on GitHub. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
2. Make a directory called `frontend` in the repository, and move your frontend files there. | ||||||
3. Make a directory called `backend` in the repository, and move your backend files there. | ||||||
4. Commit your files, and push them to your GitHub repository. | ||||||
{{</note>}} | ||||||
|
||||||
{{<note type="Exercise">}} | ||||||
Follow [the Deploying to fly.io guide](/guides/deploying/flyio/setup/) to deploy your backend. | ||||||
{{</note>}} | ||||||
|
||||||
{{<note type="Exercise">}} | ||||||
Update your frontend to talk to your deployed backend when it uses `fetch`. | ||||||
|
||||||
Follow [the Deploying to Netlify guide](/guides/deploying/netlify/) to deploy your frontend. | ||||||
{{</note>}} | ||||||
|
||||||
Make sure you can use your frontend which was deployed to Netlify. Try adding a quote, and make sure it shows up in your frontend. |
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
+++ | ||
title = "Design a frontend and backend" | ||
headless = true | ||
time = 30 | ||
facilitation = false | ||
emoji= "📖" | ||
[objectives] | ||
1="Design a frontend and backend which can communicate with each other" | ||
+++ | ||
|
||
We're going to take a frontend you've already made, and overcome one of its limitations by adding a backend. | ||
|
||
You should already have built a quote generator frontend before. The quote generator you already made used a static array of quotes that were {{<tooltip title="hard-coded">}}Hard-coding is when we write the exact data in the source code of our programme, rather than fetching it from some data source or generating it.{{</tooltip>}} in the frontend. | ||
|
||
We will add some extra functionality to our quote generator. Users will be able to add their own quotes, and then other users on other computers will be able to see the quotes that were added. | ||
|
||
Because we want to be able to store data across computers and users, we know we will need a backend. | ||
|
||
Because we want to be able to change what data we're storing, we know our backend will need to be stateful. | ||
|
||
Because we don't want to have to learn about databases right now, we're going to accept the limitation that when we restart our server, we will lose any added quotes. | ||
|
||
### Communication protocols | ||
|
||
Before we get started, we should agree how our frontend and backend are going to talk to each other. | ||
|
||
The example stateful backend we looked at before exposed this {{<tooltip title="API">}}An API - Application Programming Interface - is a description of how one program can interact with another.{{</tooltip>}}: | ||
|
||
| Path | Method | Body | Example request body | Response body | Example response body | | ||
| ---- | ------ | ---- | -------------------- | ------------- | --------------------- | | ||
| `/` | `GET` | | | A string: A quote (in quotation marks), then a dash and the author of the quote. | `"I should have been more kind." -Clive James` | | ||
| `/` | `POST` | JSON-serialised object with two keys: "quote" and "author", both of which contain strings. | `{"author": "Ibrahim", "quote": "Hello"}` | The string "ok" if successful, or a string describing an error. | `ok` | | ||
|
||
This API is asymmetric: | ||
|
||
When you POST information to it, you post structured information - the backend can easily tell which part is the author and which is the quote. | ||
|
||
When you GET information from it, the information is less structured - the server has already formatted the author and quote into a string. | ||
|
||
Pre-formatting the data may be convenient for the frontend if the backend knows exactly how it will be presented. But it takes away some flexibility from the frontend. If the frontend wanted to show the quote in italics, or the author in bold, this would be hard, because it would need to split the formatted string back up into its constituent parts. | ||
|
||
We probably instead want to change our GET endpoint to also return structured information, which the frontend can choose to format how it wants: | ||
|
||
| Path | Method | Body | Example request body | Response body | Example response body | | ||
| ---- | ------ | ---- | -------------------- | ------------- | --------------------- | | ||
| `/` | `GET` | | | JSON-serialised object with two keys, "quote" and "author", both of which contain strings. | `{"author": "Clive James", "quote": "I should have been more kind."}` | | ||
| `/` | `POST` | JSON-serialised object with two keys: "quote" and "author", both of which contain strings. | `{"author": "Ibrahim", "quote": "Hello"}` | The string "ok" if successful, or a string describing an error. | `ok` | | ||
|
||
We could also design different APIs for our frontend and backend to communicate - the important thing is that they agree on what API we will have. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think they know what POST is really. Do we want to bring anything in from servers module or no?