<div class="content">
  <h1>Simple API wrapper demo</h1>
  <p>Read more <a href="https://stanko.github.io/simple-javascript-api-wrapper/" target="_blank">on my blog</a>.</p>
  <div>
    <button id="req-people">Get People</button>
    <button id="req-person">Get Person</button>
    <button id="req-404">404 error (with JSON response)</button>
    <button id="req-json-error">Invalid JSON response</button>
  </div>
  <pre id="console"></pre>
  <div class="note">
    For demo purposes I'm using awesome <a href="https://swapi.co">Star Wars API</a>
  </div>
</div>
body {
  font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
  line-height: 1.2em;
  font-size: 16px;
  padding: 40px 20px;
  color: #24292e;
}

h1 {
  font-size: 2em;
  margin-bottom: 10px;
}

p {
  margin-bottom: 40px;
}

.content {
  max-width: 1000px;
  position: relative;
  margin: 0 auto;
}

pre {
  margin-top: 10px;
  overflow-x: auto;
  font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
  line-height: 1.3em;
  padding: 20px;
  border: 1px solid #ddd;
  border-radius: 3px;
  background: #f6f8fa;
  
  div:not(:last-child) {
    border-bottom: 1px solid #ddd;
    padding-bottom: 20px;
    margin-bottom: 20px;
  }
}

button {
  margin-right: 5px;
  margin-bottom: 5px;
  border: 1px solid #ddd;
  background: #fff;
  font-size: 14px;
  border-radius: 50px;
  padding: 5px 12px;
  transition: all 250ms;
  outline: none;

  &:hover:not(:disabled) {
    color: #2980B9;
    border-color: #2980B9;
    cursor: pointer;
  }

  &:disabled {
    opacity: 0.5;
  }
}

.note {
  margin-top: 40px;
  color: #ccc;
  font-size: 14px;
  
  a {
    color: #ccc;
  }
}
View Compiled
// ------------------------------------------------------ //
// Simple JavaScript API wrapper
// https://stanko.github.io/simple-javascript-api-wrapper
// ------------------------------------------------------ //

// For demo purposes I'm using this awesome Star Wars API
const API_URL = 'https://swapi.co/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, null, 'REQUEST_FAILED');
      }
    });
};

// ------------------------------------------------------ //
// DEMO
// PLEASE NOTE:
// this is a very naive implementation for demo purposes
// ------------------------------------------------------ //

// Define API calls
const getPeople = () => {
  return fetchResource('people');
};

const getPerson = (personId) => {
  return fetchResource(`people/${ personId }`);
};

const getJsonError = () => {
  return fetchResource('not-found');
};

// Get dom nodes

const consoleElement = document.querySelector('#console');
const buttonPeople = document.querySelector('#req-people');
const buttonPerson = document.querySelector('#req-person');
const button404 = document.querySelector('#req-404');
const buttonJsonError = document.querySelector('#req-json-error');

// Create "actions"

function requestPeople() {
  // Save button text and set it to loading
  const buttonText = this.innerHTML;
  this.innerHTML = 'Loading...';

  getPeople()
    .then(data => {
      consoleElement.innerHTML = `<div>${ JSON.stringify(data, null, 2) }</div>${ consoleElement.innerHTML }`;
      // Reset button text
      this.innerHTML = buttonText;
    })
    .catch(error => {
      consoleElement.innerHTML = `<div>${ error }</div>${ consoleElement.innerHTML }`;
      // Reset button text
      this.innerHTML = buttonText;
    });
}

function requestPerson() {
  // Save button text and set it to loading
  const buttonText = this.innerHTML;
  this.innerHTML = 'Loading...';

  getPerson(1)
    .then(data => {
      consoleElement.innerHTML = `<div>${ JSON.stringify(data, null, 2) }</div>${ consoleElement.innerHTML }`;
    // Reset button text
      this.innerHTML = buttonText;
    })
    .catch(error => {
      consoleElement.innerHTML = `<div>${ error }</div>${ consoleElement.innerHTML }`;
      // Reset button text
      this.innerHTML = buttonText;
    });
}

function request404() {
  // Save button text and set it to loading
  const buttonText = this.innerHTML;
  this.innerHTML = 'Loading...';

  getPerson('not-found')
    .then(() => {
      // Skipping as it will always fail
    })
    .catch(error => {
      consoleElement.innerHTML = `<div>${ error }</div>${ consoleElement.innerHTML }`;
      // Reset button text
      this.innerHTML = buttonText;
    });
}

function requestJsonError() {
  // Save button text and set it to loading
  const buttonText = this.innerHTML;
  this.innerHTML = 'Loading...';

  getJsonError()
    .then(() => {
      // Skipping as it will always fail
    })
    .catch(error => {
      // Escaping HTML
      const errorContent = document.createElement('div');
      errorContent.innerText = error;
      
      consoleElement.innerHTML = `<div>${ errorContent.innerHTML }</div>${ consoleElement.innerHTML }`;
      // Reset button text
      this.innerHTML = buttonText;
    });
}

// Bind actions to buttons

buttonPeople.addEventListener('click', requestPeople);
buttonPerson.addEventListener('click', requestPerson);
button404.addEventListener('click', request404);
buttonJsonError.addEventListener('click', requestJsonError);
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.