block by boeric 4950f26655187c33bedba9728e98a3c2

Github API Demo

Full Screen

Github API Demo

The Gist demos how to access the Github API to obtain metadata about a users public Repos and Gists.

In addition, it demos how to do this using only native DOM methods (as no external libraries are used). It does however use Bootstrap for some styling.

The Gist demos async-await when using fetch.

The Gist also demos how to combine the response header information with the response data payload in the fetch promise chain. The Github API implements rate control, where only certain number of API requests can be made within a certain timeframe (currently max 60 requests per hour). The parameters affecting the rate limit are provided in the response headers. To show the current rate limits, the header information needs to be available to the code that updates the UI, therefore the need to pass the headers down the fetch promise chain. At the end of the fetch call, both the parsed data and headers are provided to the caller.

Github API response headers example:

{
  "cache-control": "public, max-age=60, s-maxage=60",
  "content-type": "application/json; charset=utf-8",
  "etag": "W/\"af22d4fd297131cb0ea8f6d9893f172d\"",
  "x-github-media-type": "github.v3; format=json",
  "x-ratelimit-limit": "60",
  "x-ratelimit-remaining": "59",
  "x-ratelimit-reset": "1589696851"
}

In the visualization, please scroll to the bottom of the table to see the header information.

Please note that the Github API only delivers the first 30 Repos or Gists of a user with the non-paginated request done here. It is possible to obtain all Repos/Gists of a user, by paginated requests (repeated requests with incrementing page numbers in the query string). The Gist may be extend in the future to do that.

The Gist is alive here

index.js

/* By Bo Ericsson, https://www.linkedin.com/in/boeric00/ */

/* eslint-disable no-console, func-names, no-bitwise, no-nested-ternary, no-restricted-syntax */

// References to elements already in the DOM
const buttonGithub = document.querySelector('button.github');
const buttonGist = document.querySelector('button.gist');
const avatar = document.querySelector('.avatar');
const ownerMeta = document.querySelector('.owner-meta');
const outputContainer = document.querySelector('.output-container');
const input = document.querySelector('input');

// Set focus on the text field
input.focus();

// Generates a table with the repos/gists and displays the response headers
function generateTable(tableData, headers) {
  // Create table element
  const table = document.createElement('table');
  table.className = 'table table-striped table-sm';

  // Create the thead element
  const thead = document.createElement('thead');

  // Create the thead contents
  const headerRow = document.createElement('tr');
  Object.keys(tableData[0]).forEach((d) => {
    const th = document.createElement('th');
    th.setAttribute('scope', 'col');
    const textNode = document.createTextNode(d);
    th.appendChild(textNode);
    headerRow.appendChild(th);
  });

  // Append the header row to thead
  thead.appendChild(headerRow);

  // Append the thead to the table
  table.appendChild(thead);

  // Create the tbody element
  const tbody = document.createElement('tbody');

  tableData.forEach((rowData) => {
    // Create a table row for this row data
    const tr = document.createElement('tr');

    // For each prop in rowData, create td cells and append to tr (order is not guaranteed)
    Object.keys(rowData).forEach((cellProp) => {
      const cellValue = rowData[cellProp];
      const td = document.createElement('td');
      const textNode = document.createTextNode(cellValue);
      td.appendChild(textNode);
      tr.appendChild(td);
    });

    // Append the table row to tbody
    tbody.appendChild(tr);
  });

  // Append the tbody to the table
  table.appendChild(tbody);

  // Append the table to the container
  outputContainer.appendChild(table);

  // Create a p element to hold the title for the header json string
  const title = document.createElement('p');
  title.innerHTML = 'Response Headers';

  // Inspect the headers
  const {
    'x-ratelimit-limit': limit,
    'x-ratelimit-remaining': remaining,
    'x-ratelimit-reset': reset,
  } = headers;

  // Get current time
  const now = Date.now() / 1000;
  // Compute when the Github API rate limit will be reset
  const resetWhenSec = ~~(+reset - now);
  const resetWhenMin = ~~(resetWhenSec / 60);
  // Create string that explains that
  const explainStr = `Request limit: ${limit}, Remaining: ${remaining}, Reset time: ${reset} (in ${resetWhenSec} sec, ${resetWhenMin} min)`;
  // Then create a p elem to hold the string
  const explain = document.createElement('p');
  explain.innerHTML = explainStr;

  // Create a pre element to hold the stringified headers
  const json = document.createElement('pre');
  json.innerHTML = JSON.stringify(headers, null, 2);

  // Create a container for the header info and add the just-created children
  const headerContainer = document.createElement('div');
  headerContainer.appendChild(title);
  headerContainer.appendChild(json);
  headerContainer.appendChild(explain);

  // Append the header container to the DOM
  outputContainer.appendChild(headerContainer);
}

// Create data array of selected fields of each Github Repo, then call table renderer
function createGithubTable(data, headers) {
  // Prep data
  const tableData = data.slice()
    // Sort data in newest-to-oldest order
    .sort((a, b) => (a.updated_at < b.updated_at ? 1 : a.updated_at > b.updated_at ? -1 : 0))
    // Generate array of renderable table data
    .map((d, i) => {
      const {
        // A wealth of information is provided in the API response. Here are some of them
        // (and we're only using a few...)
        /* eslint-disable no-multi-spaces, indent */
                               // "archived": false,
                               // "clone_url": "https://github.com/boeric/airbnb-to-eslintrc.git",
                               // "created_at": "2018-11-22T02:12:10Z",
                               // "default_branch": "master"
                               // "description": "Converts Airbnb's style guide to single .eslintrc file",
                               // "disabled": false,
                               // "fork": false,
        forks,                 // "forks": 0,
                               // "forks_count": 0,
                               // "full_name": "boeric/airbnb-to-eslintrc",
                               // "has_issues": true,
                               // "has_pages": false,
                               // "has_wiki": true,
                               // "homepage": null,
                               // "id": 158630182,
                               // "language": "JavaScript",
                               // "license": null,
                               // "mirror_url": null,
        name,                  // "name": "airbnb-to-eslintrc",
                               // "open_issues": 0,
                               // "open_issues_count": 0,
                               // "owner": Object with owner info
                               // "private": false,
                               // "pushed_at": "2018-11-25T19:23:58Z",
                               // "size": 29,
                               // "ssh_url": "git@github.com:boeric/airbnb-to-eslintrc.git",
                               // "stargazers_count": 0,
        updated_at: updatedAt, // "updated_at": "2018-11-25T19:25:39Z",
        url,                   // "url": "https://api.github.com/repos/boeric/airbnb-to-eslintrc",
                               // "watchers": 0,
                               // "watchers_count": 0,
      } = d;
      /* eslint-enable no-multi-spaces, indent */

      return {
        Repo: i + 1,
        'Repo Name': name,
        'Last Updated': updatedAt,
        Url: url,
        Forks: forks,
      };
    });

  // Generate the table
  generateTable(tableData, headers);
}

// Create data array of selected fields of each Github Gist, then call table renderer
function createGistTable(data, headers) {
  // Prep data
  const tableData = data.slice()
    // Sort data in newest-to-oldest order
    .sort((a, b) => (a.updated_at < b.updated_at ? 1 : a.updated_at > b.updated_at ? -1 : 0))
    // Generate array of renderable table data
    .map((d, i) => {
      const {
        // A wealth of information is provided in the API response. Here are some of them
        // (and we're only using a few...)
        /* eslint-disable no-multi-spaces, indent */
                               // "created_at": "2020-05-16T17:43:30Z",
        description,           // "description": "Description...",
        files,                 // "files": Object with file references
                               // "id": 4950f26655187c33bedba9728e98a3c2",
                               // "owner": Object with owner info
                               // "public": true,
        updated_at,            // "updated_at": 2020-05-16T19:02:08Z",
        url,                   // "url": https://api.github.com/gists/4950f26655187c33bedba9728e98a3c2",
      } = d;
      /* eslint-enable no-multi-spaces, indent */

      return {
        Gist: i + 1,
        Description: description,
        Files: Object.keys(files).length,
        'Last Updated': updated_at,
        Url: url,
      };
    });

  // Generate the table
  generateTable(tableData, headers);
}

// Fetch json from the github API
function fetchData(url) {
  // This is an unusual construct as it combines the json returned from the API call with
  // the headers of said call
  return fetch(url)
    .then((response) => {
      const {
        headers,
        ok,
        status,
        statusText,
      } = response;

      // Test if fetch had error, if so throw
      if (!ok) {
        const errorStr = `${statusText} (${status})`;
        // This error will be handled by the caller's catch block
        throw Error(errorStr);
      }

      // Response header accumulator
      const responseHeaders = {};
      // Fill the responseHeader object with each header
      for (const header of headers) {
        const prop = header[0];
        const value = header[1];
        responseHeaders[prop] = value;
      }

      // Add the response header object to the response promise
      response.responseHeaders = responseHeaders;
      // Return the response promise
      return response;
    })
    .then((response) => {
      // Get the response header object from the response promise object
      const { responseHeaders } = response;
      // Return the settled promise
      return response.json()
        // Resolve the promise and return data and headers in a wrapped object
        .then((data) => ({ data, headers: responseHeaders }));
    });
}

// Async function to obtain the Repo/Gist data from the Github API
async function getData(type, user) {
  if (user.length === 0) {
    return;
  }

  // Clear containers
  avatar.innerHTML = '';
  ownerMeta.innerHTML = '';
  outputContainer.innerHTML = '';

  // Build url
  const url = type === 'github'
    ? `https://api.github.com/users/${user}/repos`
    : `https://api.github.com/users/${user}/gists`;

  try {
    // Disable the buttons while the data fetch is in flight
    buttonGithub.disabled = true;
    buttonGist.disabled = true;

    // Await the completion of the API call
    const wrapper = await fetchData(url);

    // Unwrap data and headers
    const { data, headers } = wrapper;

    // Log the data and headers
    console.log('data', data);
    console.log('headers', headers);

    // Enable the buttons
    buttonGithub.disabled = false;
    buttonGist.disabled = false;

    // Set focus on the text field
    input.focus();

    // If an empty array cames back, show message to user and log empty array
    if (data.length === 0) {
      ownerMeta.innerHTML = 'Not found';
      console.error(`Empty response, headers: ${headers}`);
      return;
    }

    // Get first item
    const item = data[0];
    const { owner } = item;
    const { avatar_url: avatarUrl, id, login } = owner;

    // Create the owner avatar img
    const img = document.createElement('img');
    img.src = avatarUrl;
    img.alt = login;
    avatar.appendChild(img);

    // Set owner metadata
    // Determine type
    const typeStr = type === 'github' ? 'Repos' : 'Gists';
    // Determine Repo/Gist count (Github's response limit is 30 items, but more Repos/Gists
    // could be present...)
    const countStr = data.length === 30 ? `At least ${data.length}` : `${data.length}`;
    ownerMeta.innerHTML = `User: ${login}, Id: ${id}, ${typeStr}: ${countStr}`;

    // Create tables
    if (type === 'github') {
      createGithubTable(data, headers);
    } else {
      createGistTable(data, headers);
    }
  } catch (e) {
    // Show error
    ownerMeta.innerHTML = e;

    // Enable the buttons
    buttonGithub.disabled = false;
    buttonGist.disabled = false;
  }
}

// Github button click handler
buttonGithub.addEventListener('click', function () {
  this.blur();
  getData('github', input.value.trim());
});

// Gist button click handler
buttonGist.addEventListener('click', function () {
  this.blur();
  getData('gist', input.value.trim());
});

index.html

<!doctype html>

<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Github Repos</title>
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">
  <link rel="stylesheet" href="styles.css">
  <script src="index.js" defer></script>
</head>
<body>
  <div class="top-container">
    <div>
      <div class="control-container">
        <input type="text" class="form-control input" placeholder="Github username">
        <div class="buttons">
          <button class="btn btn-primary github">Repos</button>
          <button class="btn btn-primary gist">Gists</button>
        </div>
      </div>
      <div class="tip">For example <span>boeric</span></div>
      <h5 class="owner-meta">&nbsp;</h5>
    </div>
    <div class="avatar"></div>
  </div>
  <div class="output-container">
    <h5 class="owner-meta">&nbsp;</h5>
  </div>
</body>
</html>

styles.css

body {
  box-sizing: unset;
  font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-size: 16px;
  font-style: normal;
  font-variant: normal;
  font-weight: normal;
  height: 460px;
  margin: 10px;
  max-height: 460px;
  max-width: 920px;
  padding: 10px;
  outline: 1px solid lightgray;
  overflow: scroll;
  width: 920px;
}
button {
  width: 100px;
}
img {
  width: 120px;
}
h5 {
  margin-top: 20px;
}
span {
  font-weight: bold;
}
table {
  font-size: 14px;
}
p {
  font-size: 14px;
  font-weight: bold;
}
pre {
  color: black;
  font-size: 12px;
}
.table {
  width: 1200px;
}
.input {
  width: 300px;
}
.top-container {
  display: flex;
  flex-direction: row;
  justify-content: space-between;
  height: 120px;
}
.control-container {
  display: flex;
  flex-direction: row;
  justify-content: space-between;
  width: 650px;
}
.output-container {
  margin-top: 20px;
  height: 320px;
  overflow: scroll;
}
.avatar {
  outline: 1px solid lightgray;
  width: 120px;
  height: 120px;
}
.tip {
  color: gray;
  font-style: italic;
  font-size: 12px;
}
.owner-meta {
  margin-bottom: 20px;
}