In this assignment we're going to use the free, open source, practice API https://jsonplaceholder.typicode.com. We're going to use the users and posts endpoints to list users, list a users posts, and then create a (fake) new user! We'll also practice rendering our results. Remember, the general structure of fetching data looks like this:
// 1. Invoke fetch with an API endpoint. A promise is returned.
const fetchPromise = fetch('API_ENDPOINT_GOES_HERE');
// 2. Define promise handlers with .then and .catch
fetchPromise
.then((response) => {
// 3. Check that the response is ok. If it isn't throw a useful error.
if (!response.ok) {
throw Error(`Fetch failed. ${response.status} ${response.statusText}`)
}
// 4. Start reading the response body's ReadableStream
const readingPromise = response.json();
return readingPromise
})
.then((responseBody) => {
// 5. When the response body is read, do something with it!
// What you do will depend on the API you're using
// TIP: Print the structure of the body to see what you're working with.
console.log(responseBody);
})
.catch((err) => {
// 6. Handle Errors
console.error(err);
})
For guidance on setting up and submitting this assignment, refer to the Marcy lab School Docs How-To guide for Working with Short Response and Coding Assignments.
After cloning your repository, make sure to run the following commands:
npm i
git checkout -b draft
npm t
For this assignment, we're using a Vite project! So run the command:
npm run dev
That will start Vite's dev server which will automatically update your page any time you save your files.
You have to understand that "grades" don't exist at Marcy. We only need performance data in order to know how you're doing, and make sure the people who need help get it as quickly as they can. It's ok if you didn't finish by the deadline! Just show us what you have. We'll have office hours and reviews, and we want to know what you are all struggling with so we can use those meetings effectively. This is not about grades, its about seeing what you know, and where we can help!
The most straightforward way to test your code is to test your code by hand as you work. Invoke your functions and use console.log()
to print out the results. Then, cd
into the src/
directory and use the node <file_name>
command to run your JavaScript files.
You can also create what's called a "playground" (or "sandbox") file where you import any code you need, and then mess around with that file. We've included one in the src
directory so you can see it. Run that program using node src/playground.js
.
Before submitting your code, make sure you got things right by running the provided automated tests.
You can do this using the commands:
npm test ## run the automated tests
npm run test:w ## run the automated tests and rerun them each time you save a change
You will know that you have "completed" an assignment once you have passed 75% or more of the automated tests!
We know you may know how to use async/await
syntax already, but we're going to use .then
syntax for this assignment. Why? Because we want you to understand promises deeply, and also why we invented better syntax! We'll be using async/await
in the next assignment, so don't worry, you'll get to practice that too.
In this assignment we're going to use the free, open source, practice API https://jsonplaceholder.typicode.com. We're going to use the users and posts endpoints to list users, list a users posts, and then create a (fake) new user! We'll also practice rendering our results.
First things first, we need to fetch our data. We'll do this in the functions found in src/fetch-functions.js
. These are all imported into src/main.js
where we are logging the results of these functions. Notice that each function returns a Promise so we are using .then()
calls to print out their resolved values.
checkResponseStatus()
.then((statusInfo) => {
console.log('status:', statusInfo)
});
Uncomment these function calls to see what your fetching functions return. To see the output of these console.log
statements, run the project's Vite server and view the console:
npm run dev
Complete the functions in fetch-functions.js
and you can test these functions specifically with this command:
# run tests just for this one file
npm t tests/fetch-functions.spec.js
# or just run all of the tests
npm t
At the top of the file you can see the variable:
const userUrl = 'https://jsonplaceholder.typicode.com/users'
Use this URL to send an HTTP request and get the users data. However, we don't care about the data (yet). For now, we just want to get the details of the response
object. Here are the details of this function:
- Parameters: None
- API endpoint: https://jsonplaceholder.typicode.com/users
- Output:
- A promise containing the fetch
response
'sstatus
,ok
, andurl
properties in an Object. So, something like this (not with those exact values):// A Promise that resolves to an object Promise { { status: 200, ok: true, url: https://jsonplaceholder.typicode.com/users } }
- A promise containing the fetch
The overall shape of your code should resemble this.
const getValueFromThen = () => {
// this function return's the promise created by .then
return asyncFunction().then(data => {
// this 'value' is what .then()'s promise resolves to
return 'value';
});
}
- Remember that
.then()
also returns a promise. This function should return the Promise that.then()
returns. - Remember to return from inside the
.then()
callback. The returned.then
promise resolves to whatever its callback returns.
Now, this time we ARE going to get and return the user data. Remember, the response body is a ReadableStream
which we need to read as JSON. See the example at the top of the assignment for the general structure of a fetch.
- Parameters: None
- API endpoint: https://jsonplaceholder.typicode.com/users
- Output:
- A promise containing the array of fetched users
Now that we can fetch all of the users, let's learn about a single user. In this function, we're going to fetch data about a single user by their userId
value. Write a function that takes in a userId
and gets their posts.
- Parameters:
userId
: a number identifying a usermaxNumPosts
: the number of posts to fetch from that user (default to 3 if not provided)
- API endpoint: https://jsonplaceholder.typicode.com/users/{userId}/posts
- Output:
- A promise containing an array of posts that belong to the given user, as identified by their id number. There are LOTS of posts, but you must limit the number by
maxNumPosts
, with a default value of 3.
- A promise containing an array of posts that belong to the given user, as identified by their id number. There are LOTS of posts, but you must limit the number by
And finally, we've sent fetch requests to GET data about users. Now, let's send a fetch request to create (POST) a new user. Check out the notes for how to send a fetch request with the POST verb.
For this API, after sending the request to create a new user, the response body will contain the new user you just created.
Send the request to create a new user AND return the user object provided by the API in the response body.
- Parameters:
newUserData
: an object containing ausername
andemail
property, both strings
- API endpoint: https://jsonplaceholder.typicode.com/users
- HTTP Method:
"POST"
- HTTP Method:
- Output:
- A promise containing the new
user
object from the response body. The object should contain the givenusername
andemail
properties AND theid
assigned to the user by the API.
- A promise containing the new
NOTE: Since this API isn't really saving our data, the id provided for the new user will always be the same. If you were using a real API, it would be different for each user.
Once you get your data, you actually have to use it. Now, there are 2 steps: first you need to make the rendering functions, and then you need to use them. So, for each of these questions, you will first write a rendering function in render-functions
and then you will invoke it in main.js
! This separation of concerns will keep our code nice and organized.
You may need to review some of your DOM manipulation methods, that's alright!
The first fetching function you wrote, checkResponseStatus
, sends a fetch and returns the url
, status
, and ok
properties of the response. This function, renderStatus
, should render that status information inside of the div#status
element!
-
Inputs:
statusDiv
: This will be thediv#status
element, this is what the function will modifyresponseStatusObj
: an object withurl
,status
, andok
properties (the data fromcheckResponseStatus
)
-
This function should add the following content to the provided
statusDiv
:- An
h2
tag- This will have an
id
of'status-heading'
and text content of "Info on - [url]"
- This will have an
- A
p
tag- This will have an
id
of'status-code'
- The text content depending on the
status
andok
properties of theresponseStatus
. Here are some examples:- Status code: 200, OK!
- Status code: 201, OK!
- Status code: 500, FAIL!
- Status code: 404, FAIL!
- The template looks like this:
- "Status code: [status code], [if
ok
is true then print 'OK', otherwise 'FAIL']"
- "Status code: [status code], [if
- This will have an
- An
The resulting HTML should look like this:
<!-- This is an example output -->
<div>
<h2 id="status-heading">Info on - https://jsonplaceholder.typicode.com/users</h2>
<p id="status-code">Status code: 404, FAIL!</p>
</div>
Now that you have the two helpers checkResponseStatus()
and renderStatus()
written, let's put them together!
At the top of the main()
function in main.js
, the variable statusDiv
has been created for you to grab the div#status
element.
We're going to take the data returned from checkResponseData
and render it inside of that statusDiv
using the renderStatus
function.
Here's what you should do:
- In
main.js
, invokecheckResponseStatus()
to start the fetching process. - Use
.then
to schedule a callback so that we can handle when the promise returned bycheckResponseStatus
resolves. - When the promise resolves, invoke
renderStatus
with the providedstatusDiv
and the response status data as inputs.
Hints:
Remember, you can use the .then()
function to schedule a callback to execute when a promise resolves:
asyncFunction()
.then((resolvedValue) => {
doSomethingWithData(resolvedValue);
});
When you're done, you should see this when you run your Vite development server:
The second fetching function you wrote, getUsers()
, fetches an array of users. This function should render those users as list items in the ul#users-list
element.
-
Inputs:
usersUl
: This will be theul#users-list
element to which we will add list items for each userusers
: anarray
ofuser
objects, each will have a LOT of properties from the API, but the only ones we care about for this function areusername
andid
-
For each user in
users
, this function should create a newli
element and add it to the givenusersUl
. Eachli
element should have:- A
button
with the text content"Load [username]'s posts"
. - The
button
should also have adata-user-id
attribute equal to the current user'sid
- A
The resulting HTML should look something like this:
<!-- for users [{ username: 'Bret', id: 1 }, { username: 'Joan', id: 2 }], the created html li should be -->
<ul id="users-list">
<li class="user-card">
<button data-user-id="1">Load Bret's posts</button>
</li>
<li class="user-card">
<button data-user-id="2">Load Joan's posts</button>
</li>
<ul>
- Don't forget the
data-user-id
attribute (dataset.userId
). It should correspond to each user's user id.
Now that you have the two helpers getUsers()
and renderUsers()
written, let's put them together!
At the top of the main()
function in main.js
, the variable usersUl
has been created for you to grab the ul#users-list
element.
We're going to take the data from getUsers
and render it inside of that usersUl
using the renderUsers
function.
Here's what you should do:
- In
main.js
, invokegetUsers()
to start the fetching process. - Use
.then
to schedule a callback so that we can handle when the promise returned bygetUsers
resolves. - When the promise resolves, invoke
renderUsers
with the providedusersUl
and the array of users.
When you're done, you should see this when you run your Vite development server:
The third fetching function, getUserPosts
, fetches the posts for a single user. This function should render those posts as list items in the ul#posts-list
element.
-
Inputs:
postsUl
: anHTMLElement
of aul
that this function will addli
s to, one for each postposts
: an array ofpost
(as in "blog posts") objects, each one will have anid
,title
, andbody
attribute.
-
For each post in
posts
, this function should create a newli
element and add it to the givenpostsUl
. Eachli
element should have:- An
h2
with the text content of the post'stitle
- An
p
with the text content of the post'sbody
- An
-
Additionally, each time the function is called, all previous list items held by
postsUl
should be removed. You can do this easily by settinginnerHTML = ''
.
The resulting HTML should look something like this:
<ul id="users-list">
<li>
<h2>My title 1</h2>
<p> lorem ipsum...</p>
</li>
<li>
<h2>My title 2</h2>
<p> lorem ipsum...</p>
</li>
<ul>
Note: all the titles and bodies of the api are fake latin gibberish.
Rendering posts is going to be a little different than rendering the status and rendering users. Both of those pieces of data were rendered immediately.
The posts of a single user, however, should only be rendered when one of the user buttons is clicked. Each time a different user button is clicked, the posts for that user should be fetched with getUsersPosts
and then rendered in the ul#posts-list
using renderPosts
.
Here's what you should do:
- In
main.js
set up an event listener to track clicks on the user buttons in theul#users-list
.
- How can you use delegation to minimize the number of event listeners?
- Get the
data-user-id
value from the button that was clicked and then invokegetUserPosts
using that user id. - When
getUserPosts
resolves to the fetched posts, invokerenderPosts
, providing thepostsUl
element and the fetched posts as inputs.
Note: The tests are grouped by file so each "B" step where you are working in main.js
will have a corresponding test found in main.spec.js
.
The final fetching function, createNewUser
, sends a POST request to create a new user and receives that user back. This function should render the details of the newly created user in the div#new-user
element.
- Inputs
newUserDiv
: anHTMLElement
of adiv
that we will mutate and add ournewUserInfo
newUserInfo
: an object with at least ausername
andemail
property, both are strings
This function is real simple! No arrays, just mutate the div to have an h2
and p
tag, no id's necessary.
<!-- for a user {username: 'chuck12', email: 'chuck@gamil.com'} -->
<h2>chuck12</h2>
<p>chuck@gmail.com</p>
innerHTML vs nodes:
So this is a crucial question: can you just set the innerHTML
or do you need to create isolated nodes one by one? The answer is: can you trust the data? If you know the data isn't user generated (like status codes) then innerHTML
is ok, but if you're ever entering text that could've come from a user, use nodes for safety. No malicious JS is going to sneak into our pages!
For this last one, you are challenged to think through how to put together your fetching and rendering code. In the end, you should have the following functionality:
- When we submit the form, we should take the form data to construct a
{ username, email }
object to send in a POST request to the JSON Placeholder API to create a new user. We'll get back a user object that has the same data plus anid
. - After submitting the form, we should then see the username and email appear on the screen below in the area under the form (see the
div#new-user
element) - After submitting the form, the form clears itself
- We can fill out the form as many times as we like, and the new user section will only show the latest user
A quick note about the tests. We're faking our network calls with a library called nock
. You don't make real network calls in tests, that's unreliable and slow. Unfortunately, nock
gives pretty garbage error messages. nock
will fail your test if you try to make a request for a route we haven't faked. Make sure each fetch function only fetches once and the url is exactly correct, ok?
Also, if you want to console log things in your tests, don't forget to scroll up in the terminal to see the log output!