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