Skip to content

Instantly share code, notes, and snippets.

@DavidWells
Last active March 15, 2024 08:28
Show Gist options
  • Save DavidWells/93535d7d6bec3a7219778ebcfa437df3 to your computer and use it in GitHub Desktop.
Save DavidWells/93535d7d6bec3a7219778ebcfa437df3 to your computer and use it in GitHub Desktop.
Full Github REST api in 34 lines of code
/* Ultra lightweight Github REST Client */
// original inspiration via https://gist.github.com/v1vendi/75d5e5dad7a2d1ef3fcb48234e4528cb
const token = 'github-token-here'
const githubClient = generateAPI('https://api.github.com', {
headers: {
'User-Agent': 'xyz',
'Authorization': `bearer ${token}`
}
})
async function getRepo() {
/* GET /repos/{owner}/{repo} */
return githubClient.repos.davidwells.analytics.get()
}
async function generateRepoFromTemplate({ template, repoName }) {
/* POST /repos/{template_owner}/{template_repo}/generate */
return githubClient.repos[`${template}`].generate.post({ name: repoName })
}
getRepo().then((repoInfo) => {
console.log('repo', repoInfo)
})
function generateAPI(baseUrl, defaults = {}, scope = []) {
const callable = () => {}
callable.url = baseUrl
return new Proxy(callable, {
get({ url }, propKey) {
const method = propKey.toUpperCase()
const path = scope.concat(propKey)
if (['GET', 'POST', 'PUT', 'DELETE', 'PATCH'].includes(method)) {
return (data, overrides = {}) => {
const payload = { method, ...defaults, ...overrides }
switch (method) {
case 'GET': {
if (data) url = `${url}?${new URLSearchParams(data)}`
break
}
case 'POST':
case 'PUT':
case 'PATCH': {
payload.body = JSON.stringify(data)
}
}
console.log(`Calling: ${url}`)
console.log('payload', payload)
return fetch(url, payload).then((d) => d.json())
}
}
return generateAPI(`${url}/${propKey}`, defaults, path)
},
apply({ url }, thisArg, [arg] = []) {
const path = url.split('/')
return generateAPI(arg ? `${url}/${arg}` : url, defaults, path)
}
})
}
@abesamma
Copy link

@DavidWells This is an impressive example of the power of proxies. Thanks for sharing. generateRepoFromTemplate I assume would be used to generate new repos with the repoInfo data obtained from calling getRepo correct? It doesn't seem like it has been utilized in this exampel.

@manuganji
Copy link

manuganji commented Mar 11, 2022

Removing apply function and scope argument and reorganising the function definition for easier understanding.

EDIT: read @v1vendi comment below to see why apply is useful.

/* Ultra lightweight Github REST Client */

function generateAPI(baseUrl, defaults = {}) {
  const callable = () => {};
  callable.url = baseUrl;
  return new Proxy(callable, {
    get({ url }, propKey) {
      const method = propKey.toUpperCase();
      if (["GET", "POST", "PUT", "DELETE", "PATCH"].includes(method)) {
        return (data, overrides = {}) => {
          const payload = { method, ...defaults, ...overrides };
          switch (method) {
            case "GET": {
              if (data) url = `${url}?${new URLSearchParams(data)}`;
              break;
            }
            case "POST":
            case "PUT":
            case "PATCH": {
              payload.body = JSON.stringify(data);
            }
          }
          console.log(`Calling: ${url}`);
          console.log("payload", payload);
          return fetch(url, payload).then((d) => d.json());
        };
      }
      return generateAPI(`${url}/${propKey}`, defaults);
    },
  });
}

const token = "github-token-here";
const githubClient = generateAPI("https://api.github.com", {
  headers: {
    "User-Agent": "xyz",
    Authorization: `bearer ${token}`,
  },
});

async function getRepo() {
  /* GET /repos/{owner}/{repo} */
  return githubClient.repos.davidwells.analytics.get();
}

async function generateRepoFromTemplate({ template, repoName }) {
  /* POST /repos/{template_owner}/{template_repo}/generate */
  return githubClient.repos[`${template}`].generate.post({ name: repoName });
}

getRepo().then((repoInfo) => {
  console.log("repo", repoInfo);
});

@v1vendi
Copy link

v1vendi commented Mar 11, 2022

@manuganji apply was needed in my original concept (https://gist.github.com/v1vendi/75d5e5dad7a2d1ef3fcb48234e4528cb) so you could make it even more readable, as:

async function generateRepoFromTemplate({ template, repoName }) {
  return githubClient.repos(template).generate.post({ name: repoName }); // instead of repos[`${template}`]
}

@manuganji
Copy link

manuganji commented Mar 11, 2022

@v1vendi: Thank you! Ok, so all function calls will go to the apply method? Your comment puts apply in better context and yes, makes this more readable.

@v1vendi
Copy link

v1vendi commented Mar 11, 2022

@manuganji apply in this context is a wrapper for Proxied original function. Basically we need the callable variable only to use apply on it. If we don't use this feature and stick to githubClient.repos[template].generate.post({ name: repoName }), we could remove the callable and do

return new Proxy({}, {
    get(){ /*...* }
}

there's a doc for that
https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/apply

@htunnicliff
Copy link

Any chance that there is a feasible way to create a typed version of this in TypeScript?

@About7Deaths
Copy link

About7Deaths commented Mar 11, 2022

Any chance that there is a feasible way to create a typed version of this in TypeScript?

https://github.com/johannschopplich/uncreate

@johannschopplich
Copy link

johannschopplich commented Mar 14, 2022

@htunnicliff @About7Deaths @v1vendi I've updated my typed version of this REST API approach uncreate to support all the chaining just like in this example. 🚀

@poef
Copy link

poef commented Mar 14, 2022

Nice work. I've made something similar, but more generic at https://github.com/SimplyEdit/simplyview/blob/master/js/simply.api.js
Does anyone know of any other similar approaches using Proxy?

@guillaumeduhan
Copy link

Amazing work

@kalisjoshua
Copy link

This is pretty cool. Also, very similar to something I have been using for a while.

import {objectCopy} from "./objectCopy.js"

const defaults = {
  cache: "no-cache",
  credentials: "same-origin",
  headers: {
    "Content-Type": "application/json",
  },
  mode: "same-origin",
  redirect: "follow",
  referrerPolicy: "no-referrer"
}
const METHODS = [
  "DELETE",
  "GET",
  "HEAD",
  "OPTIONS",
  "PATCH",
  "POST",
  "PUT",
]

function naiveSDK (root, config = {}, fetchAPI = fetch) {
  if (!fetchAPI) {
    throw new Error("No fetch API available.")
  }

  const defaultOptions = {...objectCopy(defaults), ...objectCopy(config)}
  const request = (method) => (route, body, options = {}) => fetchAPI(
    `${root}${route}`,
    {
      ...(body ? {body: JSON.stringify(body)} : {}),
      method,
      options: {...defaultOptions, ...objectCopy(options)},
    },
  )

  return new Proxy(METHODS, {
    get: (all, method) => all.includes(method.toUpperCase())
      ? request(method.toUpperCase())
      : () => {throw new Error(`Invalid HTTP method called: ${method}.`)},
    set (_, prop) {throw new Error(`Attempting to set property "${prop}".`)},
  })
}

export {naiveSDK}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment