JAMStack: Showing Top GitHub Repos with Netlify Functions

APIs are important element of JAMStack. Let's build an API to show GitHub repos with Netlify Functions & integrate it in React+Tailwind UI with React Query

JAMStack: Showing Top GitHub Repos with Netlify Functions

The possibilities with JAMStack is limitless. You can build any kind of integration to your website with any APIs. And those APIs are not even needed to be owned by you, of course, you have to be Authorized to use them.

As a developer some of the use cases that I can think of would be:

  • Show some active/ongoing Git Repos on your websites
  • Show your top StackOverflow answers on your website
  • etc

Let’s try to show some of the active GitHub repositories on the website.


Github Repo Demo

Here we will need a Github Profile with some Repos. If you don’t have that many repos, you can fork some popular Open Source projects to get started.

Now we need to figure out the API endpoint and authentication/authorization methods to get profile info from GitHub.

For this, we will be using the npm package @octokit/core from octokit/core.js: Extendable client for GitHub’s REST & GraphQL APIs

First, let's make our proof of concept (PoC) work by pulling the profile info in Node.js App. What would be a better example than your stuff, I will pull my profile info from github.com/pankajpatel

At this point, our PoC is to get the info with the help of @octokit/core. The following code depicts that:

const {Octokit} = require('@octokit/rest')

const api = new Octokit({auth: process.env.GITHUB_ACCESS_TOKEN})

const r = await api.request(`GET /user/repos`, {
  visibility: 'public',
  sort: 'stargazers_count'
});
console.log(r)

Which gives response like:

{
  "status": 200,
  "url": "https://api.github.com/user/repos?visibility=public&sort=stargazers_count",
  "headers": {
    "...": "..."
  },
  "data": [
    {
      "name": "multi-action-forms-example",
      "full_name": "time2hack/multi-action-forms-example",
      "private": false,
      "owner": {
        "html_url": "https://github.com/time2hack",
        "type": "Organization",
        "site_admin": false,
        "...": "..."
      },
      "html_url": "https://github.com/time2hack/multi-action-forms-example",
      "description": null,
      "fork": false,
      "created_at": "2020-12-20T12:58:57Z",
      "updated_at": "2021-01-14T08:47:44Z",
      "pushed_at": "2021-01-13T14:53:41Z",
      "homepage": "https://multi-action-forms.netlify.app/",
      "size": 19,
      "stargazers_count": 1,
      "language": "HTML",
      "has_issues": true,
      "default_branch": "main",
      "...": "..."
    },
    "...": "...another 29 repos"
  ]
}

Now let's try to filter it out based on our needs; we need the following structure fro the UI to show the top repositories:

{
  "repositories" : [{
    "stargazers_count": Number,
    "language": String,
    "name": String,
    "full_name": String,
    "html_url": String,
    "homepage": String
  }]
}

To filter out the fields and reshape the response from Github to the above structure, we will use a package called json-schema-filter

After adjusting the above schema for json-schema-filter, we will have the following code block to cleanup our response from @octokit/rest (i.e. GitHub API)

const filter = require('json-schema-filter');

const schema = {
  type: 'object',
  properties: {
    repositories: {
      type: 'array',
      items: {
        type: 'object',
        required: false,
        properties: {
          stargazers_count: { type: 'integer' },
          name: { type: 'string' },
          language: { type: 'string' },
          full_name: { type: 'string' },
          html_url: { type: 'string' },
          homepage: { type: 'string' }
        }
      }
    }
  }
};

const results = filter(
  schema,
  { repositories: r.data.filter(repo => !repo.fork) }
);

Now with the sections to fetch the repos and to filter the response, we will make a Serverless API on Netlify.


Why Serverless?

So that we don’t keep the API Server running if it is not needed and incur a large sum of billing on that server.

Furthermore on what and why serverless; please check out a brief description from Cloudflare: What is serverless computing?


How does a Serverless/Lambda function look like?
  • Serverless function is written in form of a JavaScript Module.
  • This module exports a function.
  • This function accepts event, context and callback parameters
  • The function body can do certain operations. To send the response,  either return data or call the callback function with data

With above description, a sample serverless function looks like the following:

exports.handler = function(event, context, callback) {
  // function body
  if (event.httpMethod === "POST") {
    callback(null, {
      statusCode: 200,
      body: JSON.stringify({ status: "Success" }),
    });
  }
  return {
    statusCode: 200,
    body: JSON.stringify({
      status: "OK",
      message: "Hello World"
    }),
  }
}

With the above Serverless function body, let's integrate GitHub API in the function

We are using octokit & json-schema-filter in the function. We should add them as dependencies to our repository

yarn add @octokit/rest json-schema-filter
# or
npm i -S @octokit/rest json-schema-filter
I prefer to use yarn, though you can use npm as well.

After the above, we will go ahead a create functions directory in the root of the repository.

Inside functions, let's create another directory called github-repos. Inside this directory, we will create our Serverless function.

mkdir functions
mkdir functions/github-repos
touch functions/github-repos/github-repos.js

In this file, we will add the serverless function with the body of the function to return GitHub repos of the user

const filter = require('json-schema-filter');

const schema = {
  type: 'object',
  properties: {
    repositories: {
      type: 'array',
      items: {
        type: 'object',
        required: false,
        properties: {
          stargazers_count: { type: 'integer', default: 0 },
          name: { type: 'string' },
          language: { type: 'string' },
          full_name: { type: 'string' },
          html_url: { type: 'string' },
          homepage: { type: 'string' }
        }
      }
    }
  }
};

const filterResponse = response => filter(
  schema,
  {repositories: response.data.filter(
    repo => !repo.fork
  )}
)

exports.handler = async function(event, context, callback) {
  const {Octokit} = require('@octokit/rest')

  const api = new Octokit({
    auth: process.env.GITHUB_ACCESS_TOKEN
  })

  const response = await api.request(
    `GET /user/repos`,
    {visibility: 'public'}
  )

  return {
    statusCode: 200,
    body: JSON.stringify(filterResponse(response)),
  }
}

But why stop here, let make it customisable to request repos of any user.

As GH API to get the repos of default user (owner of  GITHUB_ACCESS_TOKEN) is GET /user/repos

You can use GET /users/{username}/repos to request any user’s repos.

Let’a make this change and see how the Serverless function looks like:

exports.handler = async function(event, context, callback) {
  const {Octokit} = require('@octokit/rest')
  if (event.httpMethod === 'POST') {
    callback(null, {
      statusCode: 403,
      body: JSON.stringify({ error: 'Not Allowed' }),
    });
  }

  const user = event.queryStringParameters.user

  const api = new Octokit({
    auth: process.env.GITHUB_ACCESS_TOKEN
  })

  const endpoint = user ? `/users/${user}/repos` : '/user/repos'

  try {

    const response = await api.request(
      `GET ${endpoint}`,
      {visibility: 'public', sort: 'updated', direction: 'desc'}
    )
  
    return {
      statusCode: 200,
      body: JSON.stringify(filterResponse(response)),
    }
  } catch(e) {
    return {
      statusCode: 500,
      body: JSON.stringify(e)
    }
  }
}

Few things to note here:

  • event.queryStringParameters will provide you with the GET parameters
  • We would still respond with repos of default user if no user provided in the QueryString

As for the above Serverless endpoint, we can arrange a quick React UI with Tailwind and React Query.

  • Tailwind: A utility first CSS Library to save time on building UIs
  • ReactQuery: Library to send AJAX requests with support for caching, refetch etc.
import { useEffect, useRef, useState } from 'react';
import { useQuery } from 'react-query';
import Card from './GitHubRepoCard';

const debounce = (callback, delay = 200) => {
  let timeout
  return () => {
    clearTimeout(timeout)
    timeout = setTimeout(callback, delay)
  }
}

export const ENDPOINT = `${process.env.REACT_APP_API_BASE}/github-repos`

function App() {
  const inputRef = useRef(null)
  const [userName, setUserName] = useState('pankajpatel')

  const { isLoading, error, data, refetch } = useQuery('repoData', () =>
    fetch(`${ENDPOINT}?user=${userName}`).then(res => res.json())
  )

  useEffect(() => { refetch() }, [refetch, userName])

  if (isLoading) return 'Loading...'

  if (error) return 'An error has occurred: ' + error.message

  return (
    <div className="min-h-screen box-border p-10 bg-gradient-to-r from-green-400 to-blue-500">
      <p className='text-center text-xl text-white'>
        👇 GH Username 👇
      </p>
      <div className="flex flex-column justify-center outline m-3">
        <input
          ref={inputRef}
          list='usernames'
          type='text'
          placeholder='GH Username'
          defaultValue={userName}
          onChange={() => { 
            const value = inputRef.current.value
            debounce(setUserName(value), 250)
          }}
          className='px-4 py-2 border-2 rounded-3xl'
        />
        <datalist id="usernames">
          <option value="sindresorhus" />
          <option value="tj" />
          <option value="tannerlinsley" />
          <option value="pankajpatel" />
        </datalist>
      </div>
      <div className='flex flex-wrap flex-center justify-center justify-items-center'>
        {(data.repositories || []).map(repo => (
          <Card data={repo} key={repo.name} />
        ))}
      </div>
    </div>
  )
}

export default App;

All supporting components & utilities for the above Component can be checked here: https://github.com/pankajpatel/gh-top-repos-api


With the above code, we need a configuration file for Netlify to know

  • what is where
  • what commands to run to build the application.

Netlify reads the configuration from netlify.toml at the root of the repository.

For above API to work, we will have following configuration:

[build]
  publish = "build"
  command = "yarn build"
  functions = "functions"

[[redirects]]
  from = "/.functions/*"
  to = "/.functions/:splat"

[[redirects]]
  from = "/*"
  to = "/"

In the Above config file for netlify, we have the following sections:

  • build Tells the CI/CD pipeline of netlify about specifics of the Build process

    • publish Publish directory, in our case, it is build as CreateReactApp builds to this directory. It may differ for Next.js or Gatsby or any other site builder
    • command is to launch the build command for your project. It can be any CLI command, usually, npm script in FE Project and package.json contains a more detailed command for build
    • functions The Functions directory for Netlify Functions to build. Usually, it is functions but you can choose anything you want
  • [[redirects]] a directive to redirect requests from one endpoint to other

    • from This is the Incoming request URL pattern
    • to Where to redirect the request to
    • status (optional) status code you wanna send with redirect
  • :splat placeholder holding the value for * match in from

You can read more about netlify config file here:

File-based configuration
Netlify builds, deploys, and hosts your front end. Learn how to get started, see examples, and view documentation for the modern web platform.

You can see the demo and repository from the following links:

Github Repo Demo

Conclusion

Serverless Functions offer huge potential to do amazing things.

What would you use serverless functions for?

Let me know through comments 💬 or on Twitter at @patel_pankaj_ and/or @time2hack

If you find this article helpful, please share it with others 🗣

Subscribe to the blog to receive new posts right to your inbox.


This post is sponsored by PluralSight

PluralSight

Hey There! You have made it this far.

Would you like to subscribe via email?