Fetch Client - wbobeirne/nycda-ecommerce-server GitHub Wiki
In order to communicate with our API, we're going to want to use the fetch function in our client side javascript. This is a newer API that replaces XMLHttpRequest, and behaves a lot like $.ajax without adding half a megabyte to our script.
Making the API "Client"
Most APIs have a Javascript library that makes firing requests easy. Rather than re-implementing fetch every time we want data, we'll make a singleton for our eCommerce API that we can call easy methods on. Start by making a file in src/util/api.js
The first thing we'll want to do is export a singleton in here, and configure what the path to the API will be:
export default {
BASE_PATH: "/",
}
Making the request Function
A singleton is only as good as its methods, so let's start with the core of the api: The request function. This will be how fetch happens. First, let's have it take in some arguments:
/**
* Fires a request at the API
* @param {string} path - Path of the request, with starting /
* @param {object} options - A set of options for the request
* @param {string} options.method - GET, POST, PUT, or DELETE
* @param {string} options.args - Object of arguments to send
* @param {object} options.headers - Any additional headers to send
* @returns {Promise} Fetch promise, resolves with JSON
*/
request(path, options) {
// Do things
},
Inside of that, we'll run our very first fetch!
request(path, options) {
// Construct and return the fetch
return fetch(this.BASE_PATH + path, {
credentials: "include",
}).then((res) => {
return res.json();
}).catch((err) => {
console.error("Uncaught error:", err);
});
},
As you can see, fetch is promise based. By default it returns a response object, but the only thing we'll be interested in is the response JSON, so we'll have the promise return only the JSON of the response, not the whole thing. Unlike $.ajax, fetch doesn't convert JSON for us.
We also add a default .catch there so that we console log any errors. Fetch's catch isn't for errors like 4XX or 5XX errors, it's for if the server times out, the response is completely unintelligible, etc. If you want to handle server-sent errors, you should check for res.error.
We also add credentials: "include" to send things like cookies along to the server, so that it can identify us. Fetch also doesn't do that by default.
But we'll need to add some more stuff to make request useful:
request(path, options) {
// Setup some defaults
const method = options.method ? options.method.toUpperCase() : "GET";
let body = null;
let query = "";
const headers = {
accept: "application/json",
...options.headers || {},
};
if (method === "POST" || method === "PUT") {
// For POST and PUTs, send args as JSON body
body = JSON.stringify(options.args);
headers["Content-Type"] = "application/json";
}
else if (options.args) {
// For anything else, send args as query params
query = "?" + Object.keys(options.args).map((key) => {
return `${encodeURIComponent(key)}=${encodeURIComponent(options.args[key])}`;
}).join("&");
}
// Construct and return the fetch
return fetch(this.BASE_PATH + path + query, {
method,
headers,
body,
// Credentials, then json, catch error, and the rest...
},
These changes allow options.method, options.args, and options.headers to work. The main thing here is the args handling. POST and PUT communicate very differently from GET and DELETE. However, having a common way to communicate arguments is handy. So we add handling for both styles.
Convenience Functions
We'll save ourselves some trouble from having to specify options.method every. single. time. We'll make convenience methods for all 4 types!
/**
* Fires a GET request at the API
* @param {string} path - Path of the request, with starting /
* @param {object} options - See request for all options
* @returns {Promise} Fetch promise, resolves with JSON
*/
get(path, options) {
return this.request(path, { ...options, method: "GET" });
},
... and so on and so forth for POST, PUT, and DELETE.
What It Finally Looks Like
Using It
import API from "util/api";
API.get("/products", {
args: {
search: "Gold",
category: "mens",
},
}).then((res) => {
if (res.error) {
return getProductsFailure(err);
}
getProductsSuccess(res.data.products);
});
The Final File
export default {
BASE_PATH: "/api",
/**
* Fires a request at the API
* @param {string} path - Path of the request, with starting /
* @param {object} options - A set of options for the request
* @param {string} options.method - GET, POST, PUT, or DELETE
* @param {object} options.args - Object of arguments to send
* @param {object} options.headers - Any additional headers to send
* @returns {Promise} Fetch promise, resolves with JSON
*/
request(path, options) {
// Setup some defaults
const method = options.method ? options.method.toUpperCase() : "GET";
let body = null;
let query = "";
const headers = {
accept: "application/json",
...options.headers || {},
};
if (method === "POST" || method === "PUT") {
// For POST and PUTs, send args as JSON body
body = JSON.stringify(options.args);
headers["Content-Type"] = "application/json";
}
else if (options.args) {
// For anything else, send args as query params
query = "?" + Object.keys(options.args).map((key) => {
return `${encodeURIComponent(key)}=${encodeURIComponent(options.args[key])}`;
}).join("&");
}
// Construct and return the fetch
return fetch(this.BASE_PATH + path + query, {
method,
headers,
body,
credentials: "include",
}).then((res) => {
return res.json();
}).catch((err) => {
console.error("Uncaught error:", err);
});
},
/**
* Fires a GET request at the API
* @param {string} path - Path of the request, with starting /
* @param {object} options - See request for all options
* @returns {Promise} Fetch promise, resolves with JSON
*/
get(path, options) {
return this.request(path, { ...options, method: "GET" });
},
/**
* Fires a POST request at the API
* @param {string} path - Path of the request, with starting /
* @param {object} options - See request for all options
* @returns {Promise} Fetch promise, resolves with JSON
*/
post(path, options) {
return this.request(path, { ...options, method: "POST" });
},
/**
* Fires a PUT request at the API
* @param {string} path - Path of the request, with starting /
* @param {object} options - See request for all options
* @returns {Promise} Fetch promise, resolves with JSON
*/
put(path, options) {
return this.request(path, { ...options, method: "PUT" });
},
/**
* Fires a DELETE request at the API
* @param {string} path - Path of the request, with starting /
* @param {object} options - See request for all options
* @returns {Promise} Fetch promise, resolves with JSON
*/
delete(path, options) {
return this.request(path, { ...options, method: "DELETE" });
},
};