Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
0% found this document useful (0 votes)
45 views

Apis and Javascript

Uploaded by

bucevaldooo
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
45 views

Apis and Javascript

Uploaded by

bucevaldooo
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 68

APIs and Asynchronous

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:

What APIs, HTTP, Ajax, and JavaScript Promises are.


The difference between synchronous and asynchronous JavaScript.
How to get, set, and update data with APIs and JavaScript.
What async and await are, how they work, and when to use them.
How to serialize form data into a format APIs can understand.
How to handle API authentication and keep your credentials secure.
How to minimize the risk of cross-site scripting attacks when using third-party
APIs.

You can download all of the source code athttps://github.com/cferdinandi/apis-


source-code.

A quick word about browser compatibility


This guide focuses on methods and APIs that are supported in all modern browsers.
That means the latest versions of Edge, Chrome, Firefox, Opera, Safari, and the
latest mobile browsers for Android and iOS.

Using the code in this guide


Unless otherwise noted, all of the code in this book is free to use under the MIT
license. You can view of copy of the license at https://gomakethings.com/mit.

Let’s get started!


APIs and Asynchronous JavaScript
An API, or Application Programming Interface, allows software (or in our case,
websites and web apps) to talk to and share data with a server.

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.

// Create a Promise object


let sayHello = new Promise(function (resolve, reject) {

// In 5 seconds, resolve the Promise.


// Pass along "Hi, universe!" to any callback methods
setTimeout(function () {
resolve('Hi, universe!');
}, 5000);

});

In the example above, sayHello() is a Promise that in 5 seconds, something will


happen. You can attach functions that should run when the Promise resolves using
the Promise.then() method.

// After 5 seconds, if the Promise resolves,


// this will log "Hi, universe!" into the console
sayHello.then(function (msg) {
console.log(msg);
});

When you create a Promise, you pass in a callback function as an argument.

Inside the function, you define two parameters:resolve and reject. These are
implicit arguments the Promise passes into your callback function.

When the Promise should be considered completed, run theresolve() method.


You can pass in arguments that should get passed into the Promise.then()
method callback function into the resolve() method.

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.

// Create a Promise object


let sayHello = new Promise(function (resolve, reject) {

reject('Unable to say hi.');

// In 5 seconds, resolve the Promise.


// Pass along "Hi, universe!" to any callback methods
setTimeout(function () {
resolve('Hi, universe!');
}, 5000);

});

Now, we can add a Promise.catch() method to detect this failure and do


something about it.

// Will warn "Unable to say hi." in the console.


sayHello.then(function (msg) {
console.log(msg);
}).catch(function (error) {
console.warn(error);
});

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.

It will resolve() immediately, and pass along 1 as an argument.

// Create a Promise object


let count = new Promise(function (resolve, reject) {
resolve(1);
});

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.

// logs 1, then 2, then 3, to the console


count.then(function (num) {
console.log(num);
return num + 1;
}).then(function (num) {
console.log(num);
return num + 1;
}).then(function (num) {
console.log(num);
return num + 1;
});

You can attach Promise.then() methods at any time


One of my favorite thing about Promises is that if you assign one to a variable, you
can attach Promise.then() methods on it at any time—even after the Promise
has already resolved.

If the Promise hasn’t resolved yet, the callback will run once it does. If ithas
resolved, the callback will run immediately.

// Create a Promise that resolves immediately


let question = new Promise(function (resolve, reject) {
resolve(42);
});

// Attach a callback 5 seconds after it's resolved


setTimeout(function () {

// This will run as soon a the timeout completes,


because the Promise has already resolved
question.then(function (answer) {
console.log(answer);
});

}, 5000);

Running a callback function whether the Promise


resolves or not
The Promise.finally() runs when a Promise is settled, regardless of whether is
resolved or rejected. The callback does not receive any arguments.

In this example, we use Math.random() to randomly reject() or resolve()


the Promise. The finally() callback function runs regardless of what happens.
// Create a Promise that has a 50/50 chance of resolving or
rejecting
new Promise(function (resolve, reject) {
if (Math.random() < 0.5) {
reject(`We'll never know...`);
}
resolve(42);
}).then(function (answer) {
console.log(answer);
}).catch(function (error) {
console.warn(error);
}).finally(function () {
console.log('I run no matter what!');
});
The window.fetch() method
The window.fetch() method is used to make Ajax requests, such as calling an
API or fetching a remote resource or HTML file from a server.

Let’s look at how it works.

The basic window.fetch() method syntax


For this lesson, we’ll use JSON Placeholder to make real API requests.

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.

The fetch() method uses streams.


To get our API data as text or a JSON object, we can use one of two methods native
to the Fetch object: Body.text() and Body.json(). The Body.text() method
gets the body data as a text string, while the Body.json() method gets it as a
JSON object. Both return a Promise.

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);
});

Catching errors with the Fetch API


Because it returns a Promise, the Fetch API handles errors with the
Promise.catch() method.

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

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.

If the response.ok property is true, we can return response.json() just


like before. If not, we can throw response to trigger the Promise.catch()
method.
fetch('https://jsonplaceholder.typicode.com/postses').then(
function (response) {

// If the response is successful, get the JSON


if (response.ok) {
return response.json();
}

// Otherwise, throw an error


throw response.status;

}).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.

Instead of immediately running throw, you can instead return


response.json() or return response.text() with a chained
Promise.then() function. Inside its callback function, throw the parsed JSON or
text to trigger the Promise.catch() method, with the error data as the error
argument.
fetch('https://jsonplaceholder.typicode.com/postses').then(
function (response) {

// If the response is successful, get the JSON


if (response.ok) {
return response.json();
}

// Otherwise, throw an error


return response.json().then(function (json) {
throw json;
});

}).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().

// Otherwise, throw an error


return response.text().then(function (msg) {
throw msg;
});
Calling multiple endpoints
Sometimes, you need to call multiple API endpoints as part of a project. Let’s look
at some strategies for doing that.

The Promise.all() method accepts an array of promises. It doesn’t resolve itself


until all of the promises in the array resolve. If one of them fails, it rejects.

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.

The callback function receives an array of responses as its argument. We need to


get our JSON data from them.

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();
}));
});

From there, we can chain Promise.then() and Promise.catch() methods like


we normally would.

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.

For example, in the traditionalFn() function, we make an asynchronous API


call with the window.fetch() method. When the response comes back and is
parsed into JSON, we log it to the console. Immediately after making the call, we
also log a message to the console.

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();

When we run the traditionalFn() function, Traditional Message is logged


into the console before Traditional Fetch and the data are.

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?

How to make asynchronous code wait before


continuing
When you use the async operator before a function, you turn it into an async
function.

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.

async function asyncFn () {


await
fetch('https://jsonplaceholder.typicode.com/posts/').then(f
unction (response) {
return response.json();
}).then(function (data) {
console.log('Async Fetch', data);
});
console.log('Async Message');
}
asyncFn();

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.

An async function always returns a promise


One side-effect of using the async operator is that an async function always
returns a promise, even if you’re not actually making any asynchronous calls in it.

// This returns a promise


async function getTheAnswer () {
return 42;
}

let answer = getTheAnswer();

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 might structure your code differently with async


functions
Here’s a function that makes a call to the JSONPlaceholder API’s /posts endpoint.

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) {

// If the response is successful, get the JSON data


if (response.ok) {
return response.json();
}

// Otherwise, throw an error


throw 'Something went wrong.';

}).then(function (data) {
console.log(data);
}).catch(function (error) {
console.warn(error);
});
}

// Get the article with an ID of 3


getArticleByID(3);

If we convert getArticleByID() into an async function, we can structure it a bit


differently.

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) {

// Get the post data


let post = await
fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);

Next, we can check if the response.ok property is true. If it’s not, we’ll throw
an error.

If the response is OK, though, we’ll use theawait operator with


response.json() to get the body JSON data from the response object, and
assign it to the data variable. Again, our async function will wait for that to
complete before moving on.

Once data is set, we can log it to the console.

async function getArticleByID(id) {

// Get the post data


let response = await
fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);

// If the call failed, throw an error


if (!response.ok) {
throw 'Something went wrong.';
}

// Otherwise, get the post JSON


let data = await response.json();

// Log the data to the console


console.log(data);

}
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.

Because an async function always returns a promise, we can chain a


Promise.catch() method to it.

// Get the article with an ID of 999999


// log a warning in the console if something goes wrong
getArticleByID(999999).catch(function (error) {
console.warn(error);
});

That works, but many developers prefer to use atry...catch block inside their
async function instead.
async function getArticleByID(id) {
try {

// Get the post data


let response = await
fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);

// If the call failed, throw an error


if (!response.ok) {
throw 'Something went wrong.';
}

// Otherwise, get the post JSON


let data = await response.json();

// Log the data to the console


console.log(data);

} catch (error) {
console.warn(error);
}

// Get the article with an ID of 999999


// if there's an error, a warning is logged to the console
by the catch() block in the function
getArticleByID(999999);

Should you use Promise.then() or async/await?


I’ll be honest: I personally find async/await harder to read and write than
traditional Promise.then() chains. For most asynchronous code, I prefer to use
Promise.then().
This is absolutely a personal preference, though, so if async/await are simpler for
you, by all means use them.

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.

By default, window.fetch() requests use the GET method, which tells an


endpoint you’re trying to get data. But there are other methods, too.

The four most common are GET, POST, PUT, and DELETE.

GET - Retrieve data from an API endpoint.


POST - Submit data to an API endpoint. Usually causes something to happen
on the server.
PUT - Replace the existing data for an item with something new. This method
can often be used to create a new item if one doesn’t already exist.
DELETE - Delete the data associated with an item.

Specifying the HTTP method with the window.fetch() method

The window.fetch() method accepts an optional second argument: options.


This is an object of options and settings.

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);
});

Sending data with the window.fetch() method


Another options property that you can include with the window.fetch()
method is body. The body property holds any data you want to send as part of your
HTTP (or API) request.

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
}
});

Sending data as a JSON object

To send data as a JSON object, use the JSON.stringify() method to convert


your data into a string. For your headers['Content-type'], use
application/json as the value.

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);
});

Sending data as a query string

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.

For your headers['Content-type'], use application/x-www-form-


urlencoded as the value.
fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
body: `title=${encodeURIComponent('New Pirate
Captain')}&body=${encodeURIComponent('Arrrrrr-ent you
excited?')}&userID=3`,
headers: {
'Content-type': 'application/x-www-form-urlencoded'
}
}).then(function (response) {
if (response.ok) {
return response.json();
}
throw response;
}).then(function (data) {
console.log(data);
}).catch(function (error) {
console.warn(error);
});

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.

You typically do not need to use this property.


fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
body: `title=${encodeURIComponent('New Pirate
Captain')}&body=${encodeURIComponent('Arrrrrr-ent you
excited?')}&userID=3`,
headers: {
'Content-type': 'application/x-www-form-urlencoded'
},
credentials: 'include'
});
Serializing form data
Historically, getting data from a form into a format you could send to an API was a
bit difficult, but modern techniques make it a lot easier.

The FormData object


The FormData object provides an easy way to serialize form fields into key/value
pairs.

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>

<input type="hidden" name="userId" value="1">

<button>Submit</button>

</form>
// Get the form
let form = document.querySelector('#post');

// Get all field data from the form


// returns a FormData object
let data = new FormData(form);

The FormData object is an iterable.

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.

// logs "title", "Go to the beach", etc.


for (let [key, value] of data) {
console.log(key);
console.log(value);
}

Adding, updating, and removing items from a


FormData object
The FormData object has several methods that you can use to add, remove, and
update items.
Use the FormData.set() method to replace an existing entry, or add a new one if
an entry with that key doesn’t exist. Pass in the key and value as arguments.

// Updates the userId field with a new value


data.set('userId', '3');

// Creates a new key, "date", with a value of "4"


data.set('date', 'July 4');

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.

// Add a second "body" key to the data FormData object


data.append('body', 'Eat ice cream');

Use the FormData.delete() method to delete an entry, passing in the key as an


argument. If more than one item with that key exist, all of them are deleted.

// Delete items with the "body" key


data.delete('body');

Sending FormData to an API


The FormData object can be submitted as the value of body property of the
options object with the window.fetch() method. It has a
headers['Content-type'] value of multipart/form-data.
fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
body: data,
headers: {
'Content-type': 'multipart/form-data'
}
}).then(function (response) {
if (response.ok) {
return response.json();
}
throw response;
}).then(function (data) {
console.log(data);
}).catch(function (error) {
console.warn(error);
});

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.

Serializing FormData into a query string


To serialize a FormData object into a query string, pass it into the new
URLSearchParams() constructor. This will create a URLSearchParams object of
encoded query string values.

Then, call the URLSearchParams.toString() method on it to convert it into a


query string.
fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
body: new URLSearchParams(data).toString(),
headers: {
'Content-type': 'application/x-www-form-urlencoded'
}
}).then(function (response) {
if (response.ok) {
return response.json();
}
throw response;
}).then(function (data) {
console.log(data);
}).catch(function (error) {
console.warn(error);
});

Serializing FormData into an object


To serialize a FormData object into a plain object, we need to loop through each
entry with a for...of loop and add it to an object.

let obj = {};


for (let [key, value] of data) {
obj[key] = value;
}

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.

function serialize (data) {


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;
}
}
return obj;
}

Here’s how you would use it.


fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
body: JSON.stringify(serialize(data)),
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);
});
API Authentication
Some APIs—like the Ron Swanson Quotes Generator and Random Dog—work by
simply calling an endpoint.

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.

How to authenticate an API


To authenticate you, the API may require:

1. Your username and password.


2. A key and secret.
3. An API key or OAuth token.

These can be passed along to the API in a variety of ways.

Credentials as a query string parameter

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';

// Make the API call


fetch(`https://api.nytimes.com/svc/topstories/v2/home.json?
api-key=${apiKey}`).then(function (response) {
if (response.ok) {
return response.json();
}
throw response;
}).then(function (data) {
render(data);
}).catch(function (error) {
console.log(error);
});

Credentials with basic auth

Some APIs use a simple username/password combination for authentication using


an approach called basic auth.

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';

// Authenticate (dummy API)


fetch('https://vanillajsguides.com/api/authenticate.json',
{
headers: {
'Authorization': `Basic
${btoa(username)}:${btoa(password)}`
}
}).then(function (response) {
if (response.ok) {
return response.json();
}
throw response;
}).then(function (data) {
console.log(data);
}).catch(function (error) {
console.warn(error);
});

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.

Credentials with an authentication token

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);
});

API credentials and security


Whenever you’re dealing with credentials, you need to be concerned about security.

Your site or app should be encrypted

If you’re sending secure credentials with an API (such as a username/password or


key/secret), your site should be encrypted with an SSL certificate. It’s ok to use an
unencrypted site locally for testing, but production sites should use HTTPS. Many
APIs require this.

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

let username = 'myAwesomeUsername';


let password = 'p@ssw0rd!';

let key = 'abcdef';


let secret = '1234';

This is bad, too

localStorage.setItem('credentials', JSON.stringify({
username: 'myAwesomeUsername',
password: 'p@ssw0rd!'
}));

These should be collected in the UI when they’re needed, then immediately


discarded.

API tokens were made to be stored for reuse

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');

How to handle APIs that don’t issue short term tokens


Sometimes, APIs require credentials but don’t provide short term tokens. For
example, the Mailchimp API uses a permanent API key with basic auth to
authenticate every request.

This creates a serious problem for JavaScript developers.

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.

For situations like this, you need to setup a middleman API.

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);
});

// On a server, make the real API call with your


credentials, then return it
let apiKey = '1234';
return
fetch(`https://api.nytimes.com/svc/topstories/v2/technology
.json?api-key=my_api_key_${apiKey}`);

How to setup a middleman API


There are countless ways to set up your own server-side middleware for your APIs,
and covering all of them is well beyond the scope of this guide.

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) {

// Call the NYT API with our credentials


let apiKey = '1234';
let response = await
fetch(`https://api.nytimes.com/svc/topstories/v2/home.json?
api-key=${apiKey}`);
let data = await response.json();

// Return the data


return new Response(JSON.stringify(data), {
status: 200,
headers: new Headers({
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, HEAD,
POST, OPTIONS',
'Access-Control-Allow-Headers': '*'
})
});

};

Inside your client-side JavaScript you would use it like this.


fetch('https://nyt.my-workers-
domain.workers.dev/').then(function (response) {
if (response.ok) {
return response.json();
}
throw response;
}).then(function (data) {
console.log(data);
}).catch(function (error) {
console.warn(error);
});

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.

// The element to inject content into


var app = document.querySelector('#app');

// Some API data


var newsStories = [
//...
];

// Inject the articles into the DOM


app.innerHTML = newsStories.map(function (story) {
return `
<h1>${story.title}</h1>
<p>By ${story.author} on ${story.date}</p>
${story.content}`;
}).join('');

While the Element.innerHTML and Element.outerHTML properties are


convenient and easy ways to inject markup into the DOM, they can expose you to
cross-site scripting (or XSS) attacks when used with third-party content.

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.

There is a built-in safeguard in place, though.


Just injecting a script element won’t expose you to attacks, because the section
of the DOM you’re injecting into has already been parsed and run.

// This won't execute


let div = document.querySelector('#app');
div.innerHTML = '<script>alert("XSS Attack");</script>';

JavaScript that runs at a later time, though, will.

In this example, we attempt to load an image from an invalid source. When it fails,
the onerror event runs some malicious JavaScript.

// This WILL run


div.innerHTML = `<img src=x onerror="alert('XSS
Attack')">`;

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.

div.innerHTML = `<a href="javascript:alert('Another XSS


Attack')">Click Me</a>`;

When is this an issue?


If you’re injecting your own markup into a page, there’s little cause for concern.
The danger comes from injecting third-party or user-generated content into the
DOM.

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.

Let’s look at how.


Approach 1: inject text instead of HTML
One common solution is to use properties that render plain text instead of HTML,
such as Node.textContent or Element.innerText instead. HTML injected
using one of the text properties is automatically encoded.

// Renders a string with encoded characters


// This would show up in the DOM as an encoded string
(&lt;img src=x onerror="alert('XSS Attack')"&gt;) instead
of as an image element
div.textContent = `<img src=x onerror="alert('XSS
Attack')">`;

If you need to add markup around the content, pairNode.textContent or


Element.innerText with the document.createElement() method, and
inject it using one of the techniques from the previous lesson.

// Create your element


let content = document.createElement('h1');

// Add your content


content.textContent = `<img src=x onerror="alert('XSS
Attack')">`;

// Get the node to inject your content into


let app = document.querySelector('#app');

// Insert the content into the app


app.append(content);

If you want to replace what’s already there, you can useElement.innerHTML to


wipe the parent container first.

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')`;

// Create the link


let link = document.createElement('a');

// Add your content


link.textContent = `Click Me`;

// Sanitize and add the URL


link.href = url.replace(/javascript:/gi, '');

// Get the node to inject your content into


let app = document.querySelector('#app');

// Insert the content into the app


app.append(link);

Approach 2: encode third-party content before adding


it to the DOM
Using properties that set plain text values are great if you’re only adding text, but if
you’re adding a lot of markup around it, using document.createElement() for
every element can get tedious. Properties that inject HTML like
Element.innerHTML and Element.outerHTML properties are so much easier.

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>&lt;img src=x onerror="alert('XSS Attack')"&gt;</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>`;

This approach is lightweight, but has two drawbacks:

1. You need to remember to pass every third-party string into it.


2. It will also encode emoji. For example, the waving hand emoji (��) is
returned as &#55357;&#56395;.

Approach 3: sanitize the HTML string


If the third-party content is allowed to contain markup or characters like emoji, or
if you just don’t want to have to worry about encoding each string, you might want
to use a sanitizer library instead.

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');

// doc.body is a real HTML element with the malicious image


// No alert is thrown, though, because the elements exist
outside the DOM
console.log(doc.body);

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().

I wrote a small library, cleanHTML(), that works off of a disallow list.

// returns '<img src="x">'


let cleaned = cleanHTML(`<img src="x" onerror="alert('XSS
attacks!')">`);
app.innerHTML = cleaned;

// returns the <img> element with the onerror attribute


removed
let cleanedNodes = cleanHTML(`<img src="x"
onerror="alert('XSS attacks!')">`, true);
app.append(...cleanedNodes);

If you want something more robust, DOMPurify is an industry-leading library that


uses an allowlist and is highly configurable.
Putting it all together
To make this all tangible, let’s work on a project together.

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>

Alright, let’s get started.

Getting API Data


Here’s the endpoint for the Skwak API.

https://vanillajsguides.com/api/skwak.json

You can visit it directly if you want. There’s no authentication required.

First, let’s set up a function to pull the API data.

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.

// Get data from the Skwak API


fetch('https://vanillajsguides.com/api/skwak.json').then(fu
nction (response) {
if (response.ok) {
return response.json();
}
throw response;
}).then(function (data) {
console.log(data);
}).catch(function (error) {
console.warn('No API data found.');
});

We should get this in the console.

{
"service": "Skwak",
"username": "cannonball",
"skwaks": [
{
"content": "In the market for a new telescope.
Any recommendations?",
"date": "November 1, 1784",
"link": "https://skwak.com/cannonball/"
}
// ...
]
};

Rendering content into the DOM


Now we’re ready to actually generate some markup. First, let’s grab the#app
element and save it to a variable.
// Get the #app element
let app = document.querySelector('#app');

If our call is successful, let’s pass the data into another function,
renderSkwaks(), to actually generate and inject the markup.

// 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) {

// Render our data into the UI


renderSkwaks(data);

}).catch(function (error) {
console.warn('No API data found.');
});

Checking for swaks in the data


In the renderSkwaks() function, we should first make sure there are skwaks to
render.

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) {

// If there's no skwaks to render, display a message


if (!data.skwaks.length) {
renderNoSwaks();
return;
}

In the renderNoSkwaks() function, we’ll use the Element.innerHTML to


replace the content of the #app element with an error message.

/**
* 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) {

// Render our data into the UI


renderSkwaks(data);

}).catch(function (error) {

// If there's an error, log it


console.warn(error);

// Then display a message in the UI


renderNoSwaks();

});

Creating markup
Now we’re ready to build markup for our skwaks by updating theapp.innerHTML
property.

First, we’ll add a heading with the user’s username property.


// Render Skwak data in the DOM
function renderSkwaks (data) {

// If there's no skwaks to render, display a message


if (!data.skwaks.length) {
renderNoSwaks();
return;
}

// Otherwise, update the DOM


app.innerHTML =
`<h1>${data.username}'s skwaks</h1>`;

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.

// Otherwise, update the DOM


app.innerHTML =
`<h1>${data.username}'s skwaks</h1>
<div class="skwaks">
</div>
<p><em>Powered by ${data.service}</em></p>`;

Creating each skwak


Inside the .skwak element, we want to add each skwak.

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.

// Otherwise, update the DOM


app.innerHTML =
`<h1>${data.username}'s skwaks</h1>
<div class="skwaks">
${data.skwaks.map(function (skwak) {
// return an HTML string for each skwak
return '';
}).join('')}
</div>
<p><em>Powered by ${data.service}</em></p>`;

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>`;

Now, all of our skwaks should get displayed on the page.

Sanitizing the API data before rendering


There’s one last thing to do: sanitize the API data. Since we’re using a third-party
API, it’s important to sanitize the data before injecting it into the DOM to minimize
the risk of XSS attacks.

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>`
);

Adding some styling


This part is totally optional, but it might be nice to add a bit of styling to the UI.

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;
}

And with that, we’re done.

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.

You can find me:

On my website at GoMakeThings.com.
By email at chris@gomakethings.com.
On Twitter at @ChrisFerdinandi.

You might also like