Year #2, Week #18 đ» âŽ
New stuff we learned this week: đ€
Customizing a request with window.fetch()
- weâve already learned about and used
window.fetch
to make HTTP requests from javascript. Up until now weâve only been making GET requests. But, of course, you can make any type of request withwindow.fetch()
. In order to send a POST request, you need to make use of the optional second argument to fetch, usually called the init object, which lets you customize the request in multiple ways. One of the keys of theinit
object ismethod
, which allows you to override the default method of GET:
// this defaults to being a "GET" request
fetch(`/cats`);
// the above is the same as writing this...
fetch(`/cats`, { method: `GET` });
// here I make a `PUT` request instead of GET
fetch(`/cats`, { method: `PUT` });
- you can also pass custom headers with your request, by specifying the
headers
property, like so:
fetch(`/cats`, {
headers: {
Beep: `boop`,
},
});
- you can also supply a HTTP âbodyâ with the
.body
property:
fetch(`/cats`, {
method: `PUT`,
body: `goatbanjorodeo`,
});
- a very common need is to POST some JSON (for instance, to an API server).
To do that, we need to do 3 things with our request:
- set the method to
POST
- set a
Content-Type
header ofapplication/json
to tell the server what weâre sending - pass the JSON as a string in the body
- set the method to
- Hereâs an example of a correctly-formed POST request of some JSON data:
const myData = {
herp: "derp",
beep: true,
jim: ["jam"],
};
fetch(`/cats`, {
method: `POST`, // a.
headers: { "Content-Type": `application/json` }, // b.
body: JSON.stringify(myData), // c.
});
- there are lots more options you can pass to fetch, take a few minutes and peruse this MDN page to learn more.
âRESTâ resources
- tons and tons of apps, websites, and companies have had to build an HTTP API over the years. Because of this, certain styles and best practices have emerged in the collective wisdom of the worldwide developer community. One style of API design is called REST. Rest has flaws and shortcomings in certain scenarios, but itâs simple to understand, and for straightforward use-cases works very well.
- the basic idea behind REST is the concept of a RESOURCE. A resource in REST is some sort of âthingâ or interesting, uniquely identifiable entity in your application that you are exposing access to. A lot of times these are the ânounsâ in your application. Things like a user, a blog post, a comment, an account, a payment, etc. Many times these resources are similar to stored records in a database, but thatâs by no means a requirement.
- often times resources have RELATIONSHIPS to other resources, for instance a âblog postâ resource might have a list of references to âcommentâ resources. Or it likely has a relationship to a âauthorâ resource.
- REST APIs usually have a number of resource-based endpoints that return
either a single resource, or a collection (array) of resources. An
endpoint is a specific URL that the API responds to, like
https://api.cats.com/breeds
. - a âRESTfulâ api exposes a set of urls that map to resources in the system.
Hereâs an example. Suppose you have an API with a âuserâ resource. Itâs likely
you would want an endpoint to return a list of all users. Normally, the
endpoint for this would be the plural name of the resource, which would be
/users
. The response might look something like this:
// GET https://api.app.com/users
[
{
"id": "2100a96a-bcb3-4c8f-8dbf-c82705da51b8",
"first_name": "Jared",
"has_beard": true
},
{
"id": "b492a458-15bc-4b97-89d5-a98c1ec590ff",
"first_name": "Willow",
"has_beard": false
}
]
- the above sample response returns an ARRAY (or collection) of âuserâ
resources. Itâs also very common for REST apis to expose endpoints for
each individual resource of a given type. That would mean, continuing our
example, that the record for Willowâs user would be available at
/users/<resource-id>
, which, in our fictional example would be/users/b492a458-15bc-4b97-89d5-a98c1ec590ff
:
// GET https://api.app.com/users/b492a458-15bc-4b97-89d5-a98c1ec590ff
{
"id": "b492a458-15bc-4b97-89d5-a98c1ec590ff",
"first_name": "Willow",
"has_beard": false
}
- notice how the above response is no longer an array since we requested only
a single item, and that the URL took the form
/<plural-resource-type>/<resource-id>
. - the usual convention for creating a new resource is to
POST
some JSON to the same url as the main collection fetch endpoint. So, in our example, if we wanted to create a new user (assuming our API supported that), we would post a blob of JSON to/users
. Itâs the serverâs job then to do something different when it gets a POST request to/users
(create a new user), or when it gets a GET request (list the existing users). - POST-ing to create a new resource often returns the same resource again,
usually with a
201
status. Sometimes only the API server can generate new IDs for resources, so having a POST request spit back out the JSON for the resource can be valuable, in that it gives the newly generated ID. - there are a lot more variations and subtleties of REST apis, but theyâre all fundamentally resource-based and usually share similar conventions of URL endpoints as these simple examples.
Homework Plan (two weeks)
- 2 days review all flashcards (in your app)
- 1 day Flashcard app assignment (pt. 1)
- 1 day Flashcard app assignment (pt. 2)
- 1 day Future Project assignment
- 1 day Akron Snowmen CONTENT assignment
- 1 day Akron Snowmen LAYOUT assignment
- 2 days touch typing practice
- 8 days Execute Program homework
Flashcard App Assignment, pt. 1
- Slowly and carefully review all of the âNew Stuffâ above ^^^.
- Make sure youâve addressed any feedback I left for you on last weekâs Flashcard MR, then merge your MR.
- Connect with vscode, switch to master, pull in your merged changes, delete your old branch, and make a new branch.
- Edit your âadd a cardâ component to have state for the things the use needs to enter, so that the fields that you chose actually work, and are tracked by state. Youâll need as many pieces of state as you have form elements. Note: if you need a refresher on using React state with form elements see this new stuff.
- add a click handler to your âsubmitâ button â for now have it just
console.log()
the pieces of state youâre tracking for a new card. In the next part of the homework weâll submit this to the server, but for now, just verify that you a) enter card info, and then b) console.log all of the tracked state when the submit button is clicked. - when youâve got it logging out correctly on a submit click, commit your work.
- Next, weâre going to deal with a problem we left for ourselves from 2 weeks ago â which is that we hard-coded the dev api url into your react component. This works fine when weâre developing in dev mode, but it means that the âbuiltâ version of our site doesnât work, because it doesnât have an always running âproductionâ API to talk to.
- find the component in which youâre making the
fetch()
request to get your card data from the API. Extract the hardcodedhttp://localhost:<port>
string into a variable, and set it to one of two things.- if
process.env.NODE_ENV
is NOT"PRODUCTION"
then use the same localhost:PORT url you were using. (Look carefully, at this step, it will be easier if you test that it is NOT production, than if you test that it IS âDEVELOPMENTâ) - otherwise, use
http://api-<user>.howtocomputer.link
where<user>
is your bash username.
- if
- test your work by restarting your dev server (make sure your API server is running as well) â it should still load your cards from the dev localhost url properly.
- commit your work.
- now weâre going to add some stuff to your
package.json
to support spinning up an always-running production API server. Edit yourpackage.json
to add 2 new scripts:
{
"scripts": {
// [...]
"preserve-api": "npm run compile-api && pm2 delete \"flashcards-$USER\"; true",
"serve-api": "NODE_PORT=$PROD_API_PORT pm2 start ./dist/server/index.js --name \"flashcards-$USER\""
// [...]
}
}
- the new
"serve-api"
script is what weâll use to start a long-running production api server. It usespm2 start
which is sort of likenodemon
on steroids, in that it will run your server in the background, and re-start it if it crashes. - notice the lifecycle script
preserve-api
â this will run before"serve-api"
and recompiles your server typescript one last time, and then thepm2 delete
part just removes any running pm2 background job so there wonât try to be two. You donât have to understand this part perfectly. (In case youâre wondering the&> /dev/null
part just redirects ALL output to a special/dev/null
file which is like a trash can for output you donât care about. And the; true
is just so that this command always exits successfully, even if the previous command exited with an error) - last, weâre going to modify the
start
andbuild
scripts to setprocess.env.NODE_ENV
correctly for each one. For the"start"
script, change the script so that right after the semicolon (;
) you addNODE_ENV=DEVELOPMENT
, and for the"build"
script, change it to start withNODE_ENV=PRODUCTION
. - letâs test that the production build works by running two commands:
npm run build
andnpm run serve-api
. After both commands run, you should be able to go to your flashcard siteâs URL and it should correctly load your cards from your production API server. - if everything works, commit your work, otherwise, carefully work through the last few steps again, looking for things you might have mixed up.
Flashcard Assignment, pt. 2
- For this part of the assignment, weâre going to get your app actually adding new cards.
- connect to your repo, fire up two terminals in vscode, and start up both your web-app server, and your API server.
- Start by finding the place where you
console.log()
-ed out the state when the âadd new cardâ submit button was clicked. (Step 5 of part 1). - For the next part, we need to make a POST request with some JSON for a new
card to your api. But to do that, weâre going to need the API url again, which
is different based on whether weâre in dev mode or production mode. We took
care of this in Step 8 of the last part. We donât want to copy paste that code
(that wouldnât be DRY), so instead:
- create a new file called
./src/utils.ts
- make (and export) a function in that file called
apiUrl()
. It should take no arguments and return a string. - move the logic for figuring out the API url from step 8 of Part 1 into the function.
- import and use the function in two spots â first, the place where you fetch all of the cards from the server, and second, in the place where youâre about to POST data about a newly added card.
- create a new file called
- now that we have a nice function for getting the right api url, letâs change
our API to be more âREST-fulâ. Instead of requesting just
/
to get the cards, letâs change it so that we request the cards resource at the endpoint<url>/cards
. So, in the place where you fetch the cards, youâll need your code to look approximately like this:
fetch(`${apiUrl()}/cards`).then(() => {
/* etc. */
});
- next move back to the spot in your âadd a cardâ component where you were
formerly console-logging. Make sure youâve imported the
apiUrl()
function, and use it to send a JSON-formatted POST request to/cards
. Refer to the âNew stuffâ again for all the details on how exactly to POST json to withwindow.fetch()
. - pop open the dev tools and verify that your add-card form is sending a nice
request, by looking at the ânetworkâ tab. (Hint: you can set it to only show
you XHR requests, to make it easier to find. All window.fetch() requests
are XHR). Once you find your request in the dev tools, make sure of 3
things:
- that it is sending a
POST
request - that it is sending the correct
application/json
Content-Type header - that it is sending a properly stringified batch of json in the body
- that it is sending a
- commit your work.
- next weâre going to move over to the API server and actually implement this feature.
- the first thing we want to do is switch our listener funtion so that it only
sends back the full array of cards from the /cards endpoint. To do that,
we need to somehow know what path was requested in the HTTP request. To
do that, tiny Ryan Dahl provides a
.url
property on the incoming request object. Letâs check this out. Add aconsole.log()
statement into your server code to log outreq.url
. Take a moment and ask yourself: âwhere will I see this console.log output?â Now refresh your browser so that it makes a new request for flashcards. Did you see your console.log()? Was it where you expected it was? Do you know why? - Now that you know how to inspect the Path of the HTTP request (using
req.url
), modify your listener function code so that your API server only sends back the card data when a GET request is made to EXACTLY/cards
. - make sure your web-app is still working, and commit your work.
- OK, now we want to catch the POST requests to make a new card. Add some
logic in your request listener function to detect when:
- the method is
POST
- AND the path (url) is
/cards
- the method is
- for now, just log out some TODO message like:
console.log("TODO: actually add a card here");
. Try adding a new card from your web app. You should see your console.log with your TODO message. - if it logged out correctly, we just need to replace that console.log with some
code to extract the JSON data from the POST request, and then save it
somewhere. Simple enough! Actually, the first part of that (extract the JSON
data) is a bit gnarly to do without using a framework like express, so Iâm
going to be âGeorgeâ for you, and just give you a function that extracts the
JSON. Copy paste this function into your
./server/index.ts
file:
function getRequestJson(req: http.IncomingMessage): Promise<unknown> {
return new Promise((resolve, reject) => {
const chunks: Array<string> = [];
req.on(`data`, (chunk) => chunks.push(chunk));
req.on(`end`, () => {
try {
const json = JSON.parse(chunks.join(``));
resolve(json);
} catch (error) {
reject(error);
}
});
});
}
- study carefully the return type that function. It returns a
Promise
. Thatâs because we sort of have to wait for the full HTTP incoming request to stream to our server before we can actually parse the JSON. That could take a little while, all of the data is not necessarily available at the time we start handling the request. You donât have to even look at the inside of this function, but just be clear that it returns a Promise. I made it a Promise withunknown
inside, becauseJSON.parse()
can theoretically return anything. - ok, now, in the spot where you were console.log-ing, use the provided function
to extract the JSON from the POST body. We donât want to muck around with
.then()
functions here, because this is 2021 and we can useasync/await
. So make your listener function anasync
function, and add a line like this:
const json = await getRequestJson(req);
- then, below that line, add a line to
console.log()
out the returned JSON. Test adding a new card again from your web-app, and verify that your server is console.log-ing what you expect. - commit your work.
- now, the last thing we actually have to do is save the new card. to do this, weâre going to do several steps.
- First, find the file with your card data. Itâs probably a typescript file
called
card-data.ts
or something like that. Make a copy of this file, but rename the new copy to be: âcards.jsonâ. This should ruin the syntax highlighting and vscode will look weird and get mad at you. Thatâs OK. Delete any typescript-y parts of the file (probably just aexport default
at the top), so that all that is left is what looks like a javascript object literal. If you do it right and save your file, (and prettier is working), then prettier should take care of converting it to valid JSON. If for some reason prettier is not working for you, open a new terminal and typeprettier ./server/cards.json --write
to force it to be formatted. Donât proceed unless you get the file formatted correctly as JSON (youâll know if all of the top level keys are wrapped in double-quotes). - Second weâre going to change it so that the server file reads this JSON
instead of importing it. Go to
./server/index.ts
and remove the line that imported the card data file re renamed. Then, add a line to import the core nodefs
module. Up near the top of your file (outside your listener function), use thefs
module (particularlyrs.readFileSync
andJSON.parse()
to get your card data back into a usable javascript object, and into a named variable). Important NOTE: for the path to your cards.json file, use this:${__dirname}/../../server/cards.json
) For instance, if you used to have your card data imported ascards
orCardData
, then you would create a variable calledcards
orCardData
which would be the result of JSON-parsing the contents of yourcards.json
file. Make sure youâre using that variable for theGET /cards
request. Stop here and test your web-app, it should still work. - Third and finally - in the block of code where you are handling the âadd
cardâ incoming POST-ed JSON, after you get the parsed JSON from the request,
push the new card onto the end of the cards array, and then WRITE the data
back to the disk using
fs.writeFileSync()
. (Hint: make sure when you are writing back to the file that you remember to useJSON.stringify()
so you donât end up writing the string[object Object]
to your file. Hint 2: If you want to have your file stored with pretty-printed JSON, callJSON.stringify()
with two more arguments,null
, and2
, like this:JSON.stringify(cards, null, 2)
). - Letâs take her for a test drive! At this point, you should be able to add a new card straight from your web app.
- One last item before weâre done â we donât want
cards.json
file to be under version control so, add to your.gitignore
a line excluding that file, so it doesnât get checked in with your next commit. - test building your site and building & starting your production dev server by
doing:
npm run build
andnpm run serve-api
. Your built flashcards site should work, and you should be able to add new cards to it. - commit your work, push up a MR and slack us the URL of your flashcards site.
Akron Snowmen Assigment (CONTENT)
- Note: this assignment should be completed and submitted during the first week of the two-week chunk.
- connect to your AS repo, switch to master, pull from upstream, delete your old branch, and make a new branch. (Note: you probably will have to force delete your ânav-linksâ branch because I only merged one of them).
- create a new branchâŠ
- next, each of you has a task to integrate/synthesize all of the planning work
you all did last week for one section. Iâve posted in the
#akron-snowmen
all of your answers to last weeks homework, plus the content-writing work that Harriet and Win did a few weeks ago. - Read your assignment below, and then spend a few minutes reviewing all of the answers provided by each student that pertain to your assignment. Also refer to the content writing stuff from Harriet and Win. Then, make your best judgment of how to do your task based on everyoneâs written input, and implement the changes requested.
- Winfield:
- Youâre in charge of the pink block. Write real text for the heading, the blurb, and all 6 squares.
- If you want, you can pick icons, but thatâs not required.
- Tabitha:
- Decide on a final number and set of nav links.
- Change the text of each
- Tweak the order, if necessary.
- make sure they are pointing and scrolling to the right sections
- Willow:
- You get the hero block. Read carefully some of the copy Harriet and Win wrote, and dial in the text of the hero block until itâs good enough to show to Rod and Kristi.
- You can change any of the text in the section, including the button.
- Harriet:
- You get the âGet in Touchâ block
- Choose/write/dial-in all of the text for the whole block.
- including phone number, address, and email
- get it ready to show to Rod and Kristi
- Kiah:
- your task is the âimage and textâ blocks.
- donât worry about the images, but do get the headlines, blurbs, and button text totally dialed in and ready to show to Rod and Kristi
- Build your site, and push up a MR, and slack us both URLs.
Akron Snowmen Assignment (LAYOUT COMPONENT)
- Note: this assignment should be completed and submitted after the âCONTENTâ assignment above ^^^.
- NOTE 2: weâre going to make a totally DIFFERENT branch for this assignment than for the âCONTENTâ assignment. Donât forget to create a new branch in the next step, or you will have problems.
- connect in vscode to your AS repo, checkout master, make sure you pull from
upstream, and make a new branch called
layout-component
- create a new component in the
components/
dir, called<Layout />
. The idea behind this component is that it should handle rendering all of the stuff common to all pages, which probably includes the main nav, and the footer, and likely also the wrapper div that contains all of the blocks. The component should use the special children prop to accept JSX as children. - create your
<Layout />
component, and use it in two places: 1) use it in./pages/index.jsx
to wrap all of the blocks on the home page, and 2) use it to create a new page in a file calledour-team.tsx
. For the âinnerdsâ (children) of the new page, you donât need to make anything, just pass it some sort of placeholder, like a<h1>TODO, make a team page</h1>
. - you probably will have to rename or extract css for the Layout component.
Think carefully through which CSS should belong on the Layout component, and
make sure that is created as
./components/Layout.css
. - make sure both pages (
/
and/our-team
) render correctly. - build, submit a MR, and Slack both URLs.
Future Project Assignment
- Pick which of your two future projects youâd like to start first (web app, or brochure site).
- Ponder how to break the beginning work necessary into chunks. Stroke your beard for a while.
- Come up with a plan for 4 weeks of work on your future project. This does NOT
mean the whole project has to be done in 4 weeks, if itâs complicated it might
take a bunch more than that. This just means I want you to plan out the
first four weeks of work. Itâs also OK if you finish the task in 4 weeks. A
few thoughts:
- donât make each chunk of work too big, youâll have other HTC homework as well, donât overwhelm yourself
- remember there might be setup tasks like initializing a repo, setting up storybook (if you want it), talking to the website owner, etc. These can (and probably should) be planned into your 4-week plan as well.
- I can provide you with boilerplate for stuff like parcel/storybook/react, etc.
- You might want to use Next.js, but you donât have to. In fact, if itâs a brochure site, you are welcome to build it in plain HTML/CSS with no react, if that sounds better to you. Or, you can create it like weâre doing with Akron Snowmen.
- for your web-app, youâre almost certainly going to want to build it with React, unless you just love raw DOM scripting.
- write up your 4-week plan, and submit it in the
#homework
channel.