Apis and Javascript
Apis and Javascript
JavaScript
By Chris Ferdinandi
Go Make Things, LLC
v5.0.0
Copyright 2021 Chris Ferdinandi and Go Make Things, LLC. All Rights Reserved.
Table of Contents
1. Intro
A quick word about browser compatibility
Using the code in this guide
2. APIs and Asynchronous JavaScript
Asynchronous JavaScript
Ajax
3. JavaScript Promises
Rejecting a Promise
Chaining
You can attach Promise.then() methods at any time
Running a callback function whether the Promise resolves or not
4. The window.fetch() method
The basic window.fetch() method syntax
Catching errors with the Fetch API
The Response.ok property
5. Calling multiple endpoints
6. async and await
How to make asynchronous code wait before continuing
An async function always returns a promise
You might structure your code differently with async functions
Handling errors with async functions
Should you use Promise.then() or async/await?
7. Sending data to an API
HTTP Methods
Specifying the HTTP method with the window.fetch() method
Sending data with the window.fetch() method
Sending data as a JSON object
Sending data as a query string
Including cookies
8. Serializing form data
The FormData object
Adding, updating, and removing items from a FormData object
Sending FormData to an API
Serializing FormData into a query string
Serializing FormData into an object
9. API Authentication
How to authenticate an API
Credentials as a query string parameter
Credentials with basic auth
Credentials with an authentication token
API credentials and security
Your site or app should be encrypted
Don’t store permanent credentials in the browser or your JS
API tokens were made to be stored for reuse
How to handle APIs that don’t issue short term tokens
How to setup a middleman API
10. APIs and Cross-Site Scripting (XSS)
How it works
When is this an issue?
Approach 1: inject text instead of HTML
Sanitizing URLs
Approach 2: encode third-party content before adding it to the DOM
Approach 3: sanitize the HTML string
11. Putting it all together
Getting Setup
Getting API Data
Rendering content into the DOM
Checking for swaks in the data
Creating markup
Creating each skwak
Sanitizing the API data before rendering
Adding some styling
12. About the Author
Intro
In this guide, you’ll learn:
For example, the Twitter API let’s you get the latest tweets from a specific user.
The National Weather Service API gives you weather information. The New York
Times API let’s you get lists of top articles and news stories.
If you build web apps with JavaScript, APIs will likely be the primary way you get
data for your app and send user data back to the database.
Asynchronous JavaScript
By default, JavaScript is a synchronous language. That means that each line of code
waits for the one before it to run before executing.
Because API calls can take some time to return data, JavaScript treats these
asynchronously. That means that it calls the API, but does not wait for a response
before running the rest of the code in your script.
Instead, it sets up a callback function that will run once the API responds back with
data. This helps prevent bottlenecks and performance issues with your website or
web app.
In the next few sections, we’ll look at how that actually works.
Ajax
The term Ajax is often used to refer to asynchronous JavaScript.
It used to be an acronym (spelled AJAX in all caps) that stood for Asynchronous
JavaScript and XML. Over the last few years, JSON has replaced XML as the favored
API data format, and new techniques have replaced the old XML-based approaches,
so the spelling and meaning were changed.
If you ever hear someone talk about Ajax, they usually mean asynchronous
JavaScript.
JavaScript Promises
A Promise is a JavaScript object that represents an asynchronous function.
});
Inside the function, you define two parameters:resolve and reject. These are
implicit arguments the Promise passes into your callback function.
In the example above, we passed Hi, universe! into resolve(). This was
passed along to the Promise.then() method as the msg argument.
Rejecting a Promise
Similarly, you run the reject() method if the Promise should be considered
failed.
You can pass in any error messages or information about the rejection as
arguments. You can run a callback when a Promise fails using the
Promise.catch() method.
In the example above, let’s modify sayHello() to reject() before the timeout
completes.
});
Because reject() runs before resolve() does, the catch() callback method
will run and show the error message that was passed in.
Chaining
You can chain multiple Promise.then() methods together, and they’ll run in
sequence.
Whatever you return from the current Promise.then() method gets passed
along to the next Promise.then() method after it in the chain. Let’s create a new
Promise called count.
Now, we can chain some Promise.then() methods together. In each one one,
we’ll log num, increase it by 1, and return it to the next argument in the sequence.
In the first Promise.then() method, num is 1. In the second, it’s 2. In the last
one, it’s 3.
If the Promise hasn’t resolved yet, the callback will run once it does. If ithas
resolved, the callback will run immediately.
}, 5000);
Let’s say you wanted to get a list of posts from the API using the
https://jsonplaceholder.typicode.com/posts endpoint. First, you would
pass that into the fetch() method as an argument.
fetch('https://jsonplaceholder.typicode.com/posts');
The fetch() method returns a Promise. We can handle API responses chaining
Promise.then() and Promise.catch() methods to it. Let’s pass the
response object into our Promise.then() callback function, and log it to the
console.
fetch('https://jsonplaceholder.typicode.com/posts').then(fu
nction (response) {
// The API call was successful!
console.log(response);
}).catch(function (error) {
// There was an error
console.warn(error);
});
If you look at the response in the console, you’ll notice that theresponse.body
isn’t usable text or JSON. It’s something called aReadableStream.
In most cases, you’ll likely want JSON data. Call the method on theresponse
object, and return. We can then work with the actual response JSON in a chained
Promise.then() method.
fetch('https://jsonplaceholder.typicode.com/posts').then(fu
nction (response) {
// The API call was successful!
return response.json();
}).then(function (data) {
// This is the JSON from our response
console.log(data);
}).catch(function (error) {
// There was an error
console.warn(error);
});
However, the Promise only rejects and triggers the Promise.catch() method if
the request fails to resolve. If there’s a response from the server, even if it’s a 404
or 500 error, the Promise.then() methods still run.
For example, in this request below, I’ve misspelled /posts as /postses, causing a
404 error.
fetch('https://jsonplaceholder.typicode.com/postses').then(
function (response) {
// The API call was successful
// (wait, it was?)
console.log(response.status);
return response.json();
}).then(function (data) {
// This is the JSON from our response
console.log(data);
}).catch(function (error) {
// There was an error
console.warn(error);
});
If you try this yourself, you’ll notice that an empty JSON object is logged, and our
warning doesn’t show in the console.
The Response.ok property returns a boolean, with a value of true if the response
has a status code between 200 and 299, and false if it does not.
}).then(function (data) {
// This is the JSON from our response
console.log(data);
}).catch(function (error) {
// There was an error
console.warn(error);
});
While that triggers the Promise.catch() handler, the error parameter is the
response object. Sometimes the response.status code or the
response.statusText are all you need. But often, details about the error are in
the response.body body.
}).then(function (data) {
// This is the JSON from our response
console.log(data);
}).catch(function (error) {
// There was an error
console.warn(error);
});
Some APIs return a simple string error message instead of an object. If that’s the
case, you’ll get a JSON error in the console.
For APIs that don’t return an error object, use response.text() instead of
response.json().
This is useful when you have two or more APIs, and need data from both to
continue.
For example, the the JSONPlaceholder API has a /posts endpoint that returns a
list of blog posts, and a /users endpoint that returns a list of post authors. We
want to generate a list of blog posts from the /posts endpoint, and merge author
data from the /users endpoint to generate a UI.
Since the window.fetch() method returns a promise, we can create an array with
window.fetch() calls to our two endpoints, and pass it into the
Promise.all() method.
Promise.all([
fetch('https://jsonplaceholder.typicode.com/posts'),
fetch('https://jsonplaceholder.typicode.com/users')
]);
Next, we would chain a Promise.then() handler to it. This will run after both
window.fetch() methods resolve.
Promise.all([
fetch('https://jsonplaceholder.typicode.com/posts'),
fetch('https://jsonplaceholder.typicode.com/users')
]).then(function (responses) {
console.log(responses);
});
We’ll again return a Promise.all() method, this time using the Array.map()
method to loop through each response and call the response.json() method
on it. Once they all resolve, the result will be an array of JSON objects.
Promise.all([
fetch('https://jsonplaceholder.typicode.com/posts'),
fetch('https://jsonplaceholder.typicode.com/users')
]).then(function (responses) {
// Get a JSON object from each of the responses
return Promise.all(responses.map(function (response) {
return response.json();
}));
});
The data parameter receives an array with the resolved JSON objects in the same
order that the API calls were made in the Promise.all() method.
Promise.all([
fetch('https://jsonplaceholder.typicode.com/posts'),
fetch('https://jsonplaceholder.typicode.com/users')
]).then(function (responses) {
// Get a JSON object from each of the responses
return Promise.all(responses.map(function (response) {
return response.json();
}));
}).then(function (data) {
// You would do something with both sets of data here
// data[0] is the /posts endpoint
// data[1] is the /users endpoint
console.log(data);
}).catch(function (error) {
// if there's an error, log it
console.log(error);
});
Because the Promise.catch() method will run if any of the APIs fails, you
should only use this approach when you absolutely need all of the APIs to succeed
to complete your task.
If you can progressively update your UI or complete other tasks as APIs successfully
return, that’s often the better approach.
async and await
The async and await operators allow you to treat asynchronous code like
synchronous code.
function traditionalFn () {
fetch('https://jsonplaceholder.typicode.com/posts/').then(f
unction (response) {
return response.json();
}).then(function (data) {
console.log('Traditional Fetch', data);
});
console.log('Traditional Message');
}
traditionalFn();
Because fetch() is asynchronous, the rest of our function does not wait for it to
complete before continuing. But we if we wanted to do just that?
Inside an async function, you can use the await operator before asynchronous code
to tell the function to wait for that operation to complete before moving on.
In this example, we’ve turned asyncFn() into an async function. We’ve also
prefaced the window.fetch() call with the await operator.
When this runs, Async Fetch and the returned data are logged into the console
before Async Message. The function waited for the window.fetch() Promise
to settle before continuing.
Here, answer does not have a value of 42. Instead, it’s value is a resolved promise
that you can use Promise.then() and Promise.catch() with.
// logs 42 into the console
answer.then(function (data) {
console.log(data);
});
You pass in a post ID as an argument, and it fetches the data, parses it to JSON, and
handles errors.
/**
* Get an article by its ID
* @param {Integer} id The article ID
*/
function getArticleByID (id) {
fetch(`https://jsonplaceholder.typicode.com/posts/${id}`).t
hen(function (response) {
}).then(function (data) {
console.log(data);
}).catch(function (error) {
console.warn(error);
});
}
First, let’s use the await operator with our window.fetch() call, and assign the
returned response to the response variable. Our async function will wait until the
response is returned before continuing.
async function getArticleByID(id) {
Next, we can check if the response.ok property is true. If it’s not, we’ll throw
an error.
}
Handling errors with async functions
In our async getArticleByID() function, we throw an error, but don’t actually
catch it or handle it anywhere. There are a few ways you can deal with this.
That works, but many developers prefer to use atry...catch block inside their
async function instead.
async function getArticleByID(id) {
try {
} catch (error) {
console.warn(error);
}
In situations where you actually need to wait for asynchronous code to resolve
before continuing, though, async/await is the correct choice.
Sending data to an API
In previous sections, we used the window.fetch() method to get data from an
API. But what if you wanted to send data to an API instead?
HTTP Methods
HTTP methods are verbs that describe the type of request you’re making to an
endpoint.
The four most common are GET, POST, PUT, and DELETE.
To use an HTTP method other than GET, pass in an object with the method key,
and use your desired HTTP method as the value.
// Make a POST request
fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST'
}).then(function (response) {
if (response.ok) {
return response.json();
}
throw response;
}).then(function (data) {
console.log(data);
}).catch(function (error) {
console.warn(error);
});
Depending on the endpoint, this data may be sent as a JSON object or a query
string. Some APIs allow both types, while some require just one or the other.
API requests are sent with headers that include information about the request.
When sending data with the window.fetch() method, you will need to specify
the Content-type as a property of the headers property in the options object.
This tells the API if the data you sent is JSON or a query string.
fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
body: '', // The data
headers: {
'Content-type': '' // The type of data you're
sending
}
});
Note: the JSON Placeholder API request that you also specify the charset as UTF-8.
Most APIs do not need this.
fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
body: JSON.stringify({
title: 'New Pirate Captain',
body: 'Arrrrrr-ent you excited?',
userId: 3
}),
headers: {
'Content-type': 'application/json; charset=UTF-8'
}
}).then(function (response) {
if (response.ok) {
return response.json();
}
throw response;
}).then(function (data) {
console.log(data);
}).catch(function (error) {
console.warn(error);
});
To send data as a query string, include the query string as the value of thebody
property. Any properties that may have spaces or special characters in them should
be passed into the encodeURIComponent() method to encode it.
Including cookies
By default, requests send with the window.fetch() method to an API on a
different domain will not include any cookies from the site you’re currently on. If
you need to send those cookies, use the credentials property in the options
object, with a value of include.
This is typically needed when a user is logged in with a session cookie, and that
session data is needed as part of the API authentication.
Use the new FormData() constructor to create a new FormData object, passing in
the form to serialize as an argument. Form fields must have a name attribute to be
included in the object. Otherwise, they’re skipped. The id property doesn’t count.
<form id="post">
<label for="title">Title</label>
<input type="text" name="title" id="title" value="Go to
the beach">
<label for="body">Body</label>
<textarea id="body" name="body">Soak up the sun and
swim in the ocean.</textarea>
<button>Submit</button>
</form>
// Get the form
let form = document.querySelector('#post');
You can loop through it using a for...of loop. Each entry is an array of
key/value pairs.
// logs...
// ["title", "Go to the beach"]
// ["body", "Soak up the sun and swim in the ocean."]
// ["userId", "1"]
for (let entry of data) {
console.log(entry);
}
You can use array destructuring to assign the key and value to their own variables
within the for...of loop.
Use the FormData.append() method to add a new entry, passing in thekey and
value as arguments. If an item with that key already exists, another one is added
and the existing one is unaffected.
However, many API endpoints don’t accept data with that content type, and require
you to send data as a stringified object or query string.
Fortunately, you can convert the FormData object into one of those formats pretty
easily.
With this simple approach, if there’s more one form field with the same name, the
original value will get overwritten.
To account for this, we need to check if the key already exists in the obj. If it does,
we want to convert it to an array and Array.push() the new value into it.
let obj = {};
for (let [key, value] of data) {
if (obj[key] !== undefined) {
if (!Array.isArray(obj[key])) {
obj[key] = [obj[key]];
}
obj[key].push(value);
} else {
obj[key] = value;
}
}
Here’s a helper function you can use to convert aFormData object into a plain
object. Pass in the FormData object as an argument.
Others—like the New York Times and many endpoints for the GitHub API—require
you to authenticate who you are before you can make API calls.
Some APIs accept an API key or other credentials as a query string parameter on
the endpoint URL.
For example, here’s the endpoint for the New York Times API.
// The API Key
// DO NOT store credentials in your JS file like this
let apiKey = '1234';
With basic auth, you include an Authorization property on the headers key in
the options object. For it’s value, you use the following pattern:Basic
USERNAME:PASSWORD.
Both the username and password need to be base64 encoded, which we can do with
the window.btoa() method.
// The username and password
// DO NOT store credentials in your JS file like this
let username = 'myUsername';
let password = '1234';
With basic auth, you typically make your API call to a specific authorization
endpoint. The returned data usually includes a token that expires after some
period of time that you’ll use for any call you make to other endpoints.
API tokens are designed to be short term credentials you can use to authenticate
API calls after authenticating yourself some other way (typically with a key and
secret or username and password).
They can last as little as a few minutes, as long as months, or in some cases,
indefinitely.
With token-based auth, you again include an Authorization property on the
headers key in the options object. For it’s value, you use the following pattern:
Bearer TOKEN. No encoding is required.
fetch('https://vanillajsguides.com/api/skwak.json', {
headers: {
'Authorization': `Bearer ${token}`
}
}).then(function (response) {
if (response.ok) {
return response.json();
}
throw response;
}).then(function (data) {
console.log(data);
}).catch(function (error) {
console.warn(error);
});
Many web hosts provide SSL certificates at no cost or for a small fee. If you’re
comfortable with command line, you can also install one for free using Let’s
Encrypt.
Don’t store permanent credentials in the browser or your JS
Permanent credentials, such as a username and password or key and secret, should
never be stored in the browser or included in your JavaScript.
This is BAD
localStorage.setItem('credentials', JSON.stringify({
username: 'myAwesomeUsername',
password: 'p@ssw0rd!'
}));
API tokens, which are usually obtained by supplying other credentials, were
designed specifically to be saved for reuse. Because they expire and often grant
limited permissions, they often create a smaller risk footprint than other user
credentials do.
The most secure way to store them is with a cookie with thesecure flag enabled,
but window.localStorage() and window.sessionStorage() are also
options.
// This is usually OK
// Short term API tokens were designed to be stored locally
document.cookie = `token=1234; max-age=${60 * 60 * 24 *
14}; secure;`;
localStorage.setItem('token', '1234');
sessionStorage.setItem('token', '1234');
If you include permanent credentials in your JS, anyone who knows how to view
source or view requests in their browser’s Developer Tools can view those
credentials, steal them, and use them to access the API as if they were you.
A middleman API is an endpoint you setup on a server. You store your API
credentials securely on the server, and call the middleman API from your JavaScript.
The middleman API then the real API call with your secure credentials.
When it gets a response, it sends back the data, optionally filtering out any
information you don’t want exposed publicly.
Here’s some pseudo-code showing how you might do that with the NYT API.
// In your JavaScript, call the middleman API
// You don't provide any API credentials
fetch('https://my-middleman-api.com').then(function
(response) {
if (response.ok) {
return response.json();
}
throw response;
}).then(function (data) {
console.log(data);
}).catch(function (error) {
console.warn(error);
});
But one of the simplest ways, if you’re already comfortable with vanilla JS, is to use
Cloudflare Workers.
Cloudflare Workers are what’s called serverless functions (which is really just a fancy
marketing phrase for on-demand server functionality managed by someone else).
Unlike most serverless offerings, you can author Cloudflare Workers in vanilla JS.
They offer a free version (at time of writing), and have a GUI that you can use to
manage your functions.
Here’s a super simple version for the NYT API, without any error handling.
// Listen for API requests
addEventListener('fetch', function (event) {
event.respondWith(handleRequest(event.request));
});
/**
* Respond to the request
* @param {Request} request
*/
async function handleRequest(request) {
};
If you want to dig more into this topic, I’ve written articles onhow to get started
with Cloudflare Workers, how to secure your serverless functions, and how to use
environment variables.
APIs and Cross-Site Scripting (XSS)
One of the easiest ways to add API content to the DOM is with the
Element.innerHTML and Element.outerHTML properties.
In this lesson, you’ll why that is and how to minimize your risk.
How it works
The idea behind an XSS attack with Element.innerHTML or
Element.outerHTML is that malicious code gets injected into your site and then
executed. This is possible because those properties render complete markup and
not just text.
In this example, we attempt to load an image from an invalid source. When it fails,
the onerror event runs some malicious JavaScript.
In this case, it’s just alerting a message. But in a real world example, the code might
scrape sensitive data from our site and send it to a third-party source.
Links are another potential attack vector. If an href attribute is set from third-
party data, a user with malicious intent can prefix the URL with javascript: to
run code when the user clicks the link.
If you’re adding content to a page that you didn’t write, you should sanitize
and encode it to protect yourself from XSS attacks.
app.innerHTML = '';
Sanitizing URLs
Injecting text is great for body content, but it does not protect you when using
third-party data as the href attribute on a link.
For that, you need to remove javascript: from the string with the
String.replace() method.
// A dangerous URL
let url = `javascript:alert('Another XSS Attack')`;
To make them safer to use, you can encode the content (as in, convert markup to
plain text) before injecting it.
If your third-party code is not allowed to contain any markup,you can use a helper
method to remove markup from the code: encodeHTML().
/**
* Encode the HTML in a user-submitted string
* https://portswigger.net/web-security/cross-site-
scripting/preventing
* @param {String} str The user-submitted string
* @return {String} str The sanitized string
*/
function encodeHTML (str) {
return str.replace(/data:/gi,
'').replace(/javascript:/gi, '').replace(/[^\w-_. ]/gi,
function (c) {
return `&#${c.charCodeAt(0)};`;
});
}
This works by finding every character that’s not whitespace (), a dash (-), or an
underscore (_), and replacing it with an encoded HTML string instead. As a result,
those characters are rendered as literal text strings rather than as HTML.
It also uses the String.replace() method to find and replace all instances of
javascript: and data:. Otherwise, they could be used to run JS when a link is
clicked.
let thirdPartyString = `<img src=x onerror="alert('XSS
Attack')">`;
let thirdPartyURL = `javascript:alert('Another XSS
Attack')`;
// Renders...
// <p><img src=x onerror="alert('XSS Attack')"></p>
// <p><a href="alert('Another XSS Attack')">View My
Profile</a></p>
div.innerHTML =
`<p>${encodeHTML(thirdPartyString)}</p>
<p><a href="${encodeHTML(thirdPartyURL)}">View My
Profile</a></p>`;
The DOMParser() method converts an HTML string into real HTML without
rendering in the actual DOM. As a result, any malicious code is not executed (and
won’t be until those HTML elements are injected into the UI).
let parser = new DOMParser();
let doc = parser.parseFromString(`<img src="x"
onerror="alert('XSS attacks!')">`, 'text/html');
Sanitizer libraries use the DOMParser() method to create HTML elements from
your HTML string, then loop through each element and remove any attributes,
properties, and values that are not included in an allowlist or are explicitly
forbidden on a disallow list.
You can pass your entire HTML string into a sanitizer library, and it will return
either a sanitized string that you can use with an HTML string property, or the
sanitized elements that you can inject into the DOM with a method like
Element.append().
Skwak is fictional “Twitter for pirates” service that I made up for this guide. We’re
going to use their API to get a list of skwaks from a user named cannonball.
The starter template and complete project code are included in the source code on
GitHub.
Getting Setup
The template has some starting markup: an #app element with a loading message
that should get replaced once the API data is returned.
<div id="app">
<p>Loading the latest skwaks...</p>
</div>
https://vanillajsguides.com/api/skwak.json
Some APIs do a great job documenting the data that’s sent back, while others don’t.
The Skwak API has no documentation, so let’s use the window.fetch() method
to get data from the server, and log the results to the console so that we can see
what data comes back.
If no data is found, we’ll throw a warning into the console for now.
{
"service": "Skwak",
"username": "cannonball",
"skwaks": [
{
"content": "In the market for a new telescope.
Any recommendations?",
"date": "November 1, 1784",
"link": "https://skwak.com/cannonball/"
}
// ...
]
};
If our call is successful, let’s pass the data into another function,
renderSkwaks(), to actually generate and inject the markup.
}).catch(function (error) {
console.warn('No API data found.');
});
We can check the length of the data.skwaks array, and if it’s 0, show an error
message. It’s also a good idea to show an error if the API call fails, so let’s make
this a function we can call in both places: renderNoSkwaks().
// Render Skwak data in the DOM
function renderSkwaks (data) {
/**
* Update the DOM when the API is down or no skwaks exist
*/
function renderNoSwaks () {
app.innerHTML = '<p>Sorry matey! No skwaks at this
time. Arr...</p>';
}
We’ll also call this function when our API call is unsuccessful.
// Get the API data
fetch('https://vanillajsguides.com/api/skwak.json').then(fu
nction (response) {
if (response.ok) {
return response.json();
}
throw response;
}).then(function (data) {
}).catch(function (error) {
});
Creating markup
Now we’re ready to build markup for our skwaks by updating theapp.innerHTML
property.
Next, let’s add a wrapper to hold all of the skwaks, and give it a class of.skwak.
Let’s also add a “Powered by…” message, and use the data.service property to
dynamically add the name.
There are lots of ways to loop through and generate content from an array, but I
personally the easiest is to use the Array.map() and Array.join() methods.
We’ll use Array.map() to create an HTML string for each skwak in the array.
Then, we’ll use the Array.join() method to combine them all together into a
single string that we can inject into the DOM.
First, let’s wrap each skwak in an article element with the .skwak class. We can
use this for styling later.
Inside each .skwak, we’ll add a link, and use the skwak.link property for the
href. We’ll use the date the skwak was published as the link text, with the
skwak.date property. Let’s also give the link a .skwak-date class for styling.
Below that, we’ll add the skwak content with the skwak.content property. We’ll
wrap this in an element with the .skwak-content class for styling purposes.
// Otherwise, update the DOM
app.innerHTML =
`<h1>${data.username}'s skwaks</h1>
<div class="skwaks">
${data.skwaks.map(function (skwak) {
return `
<article class="skwak">
<a class="skwak-date"
href="${skwak.link}">${skwak.date}</a>
<div class="skwak-
content">${skwak.content}</div>
</article>`;
}).join(''}
</div>
<p><em>Powered by ${data.service}</em></p>`;
Let’s drop in the cleanHTML() helper function, and pass the complete HTML
string into it.
// Otherwise, update the DOM
app.innerHTML =
cleanHTML(
`<h1>${data.username}'s skwaks</h1>
<div class="skwaks">
${data.skwaks.map(function (skwak) {
return `
<article class="skwak">
<a class="skwak-date"
href="${skwak.link}">${skwak.date}</a>
<div class="skwak-
content">${skwak.content}</div>
</article>`;
}).join('')}
</div>
<p><em>Powered by ${data.service}</em></p>`
);
I’m going to add a bit of padding to each .skwak, as well as a light gray border on
the bottom of each one. I’m also going to make the .skwak-date a dark gray with
no underline. I’ll change the color to blue and underline it when it’s hovered or
active.
.skwak {
border-bottom: 1px solid #e5e5e5;
padding: 1.5em 0;
}
.skwak-date {
color: #808080;
text-decoration: none;
}
.skwak-date:active,
.skwak-date:hover {
color: #0088cc;
text-decoration: underline;
}
Congratulations! You just used the window.fetch() method and API data to
dynamically build some web content.
About the Author
Hi, I’m Chris Ferdinandi. I believe there’s a simpler, more resilient way to make
things for the web.
I’m the author of the Vanilla JS Pocket Guide series, creator of the Vanilla JS
Academy training program, and host of the Vanilla JS Podcast. My developer tips
newsletter is read by thousands of developers each weekday.
I love pirates, puppies, and Pixar movies, and live near horse farms in rural
Massachusetts.
On my website at GoMakeThings.com.
By email at chris@gomakethings.com.
On Twitter at @ChrisFerdinandi.