For handling API calls I have a small snippet I'm copying from project to project. I decided to clean it up, make more generic and share it. It is intended to be a starting point, so you might want to customize it to your custom needs.
What it does? #
It is a simple wrapper around
native fetch
.If you need a polyfill isomorphic-fetch is a great one.
- For successful requests it will parse the response and return it.
- When HTTP error occursIt detects errors based on request's HTTP status. it will throw a custom error with status code, error message and response (parsed if it is a JSON).
- If request never gets resolved, same custom error will be thrown but
response will be set to
null
and status code toREQUEST_FAILED
.
Please note that function will return a Promise so you need to handle how it resolves.
Code #
It is written as a native ES2015 module, which means you may need to transpile it depending on your browser support policy.
// ------------------------------------------------------ //
// Simple JavaScript API wrapper
// https://muffinman.io/blog/simple-javascript-api-wrapper
// ------------------------------------------------------ //
// For demo purposes I'm using this awesome Star Wars API
const API_URL = 'https://swapi.tech/api';
// Custom API error to throw
function ApiError(message, data, status) {
let response = null;
let isObject = false;
// We are trying to parse response
try {
response = JSON.parse(data);
isObject = true;
} catch (e) {
response = data;
}
this.response = response;
this.message = message;
this.status = status;
this.toString = function () {
return `${ this.message }\nResponse:\n${ isObject ? JSON.stringify(this.response, null, 2) : this.response }`;
};
}
// API wrapper function
const fetchResource = (path, userOptions = {}) => {
// Define default options
const defaultOptions = {};
// Define default headers
const defaultHeaders = {};
const options = {
// Merge options
...defaultOptions,
...userOptions,
// Merge headers
headers: {
...defaultHeaders,
...userOptions.headers,
},
};
// Build Url
const url = `${ API_URL }/${ path }`;
// Detect is we are uploading a file
const isFile = options.body instanceof File;
// Stringify JSON data
// If body is not a file
if (options.body && typeof options.body === 'object' && !isFile) {
options.body = JSON.stringify(options.body);
}
// Variable which will be used for storing response
let response = null;
return fetch(url, options)
.then(responseObject => {
// Saving response for later use in lower scopes
response = responseObject;
// HTTP unauthorized
if (response.status === 401) {
// Handle unauthorized requests
// Maybe redirect to login page?
}
// Check for error HTTP error codes
if (response.status < 200 || response.status >= 300) {
// Get response as text
return response.text();
}
// Get response as json
return response.json();
})
// "parsedResponse" will be either text or javascript object depending if
// "response.text()" or "response.json()" got called in the upper scope
.then(parsedResponse => {
// Check for HTTP error codes
if (response.status < 200 || response.status >= 300) {
// Throw error
throw parsedResponse;
}
// Request succeeded
return parsedResponse;
})
.catch(error => {
// Throw custom API error
// If response exists it means HTTP error occured
if (response) {
throw new ApiError(`Request failed with status ${ response.status }.`, error, response.status);
} else {
throw new ApiError(error.toString(), null, 'REQUEST_FAILED');
}
});
};
export default fetchResource;
Usage #
fetchResource
accepts two arguments, mandatory URL and optional options to be passed to the fetch
request.
If you wrap each API request in a function with it's specific options it will be easier to maintain.
Few examples:
// Simple get request
function getUsers() {
return fetchResource('users');
}
// Post request with payload
function signIn(username, password) {
return fetchResource('signin', {
method: 'POST',
body: {
username,
password,
},
});
}
// Put request, with file data and custom headers
function uploadAvatar(userId, file) {
return fetchResource(`users/${ userId }/avatar/`, {
method: 'PUT',
body: file,
headers: {
'Content-Type': file.type,
},
});
}
Then you call newly created API functions,
they will return a Promise, so you need to define then
(and catch
for errors).
getUsers()
.then(userData => {
// Do something with the "data"
})
.catch(error => {
// Handle error
// error.message (error text)
// error.status (HTTTP status or 'REQUEST_FAILED')
// error.response (text, object or null)
})
Demo #
I've cooked a small demo to show it in practice, in which I used free Star Wars API.
Feel free to play with it on CodePen.
Comments (4)