Skip to content

Instantly share code, notes, and snippets.

@boeric
Last active May 23, 2020 19:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save boeric/4950f26655187c33bedba9728e98a3c2 to your computer and use it in GitHub Desktop.
Save boeric/4950f26655187c33bedba9728e98a3c2 to your computer and use it in GitHub Desktop.
Github API Demo

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

<!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>
/* 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());
});
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;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment