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
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.
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
andcallback
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 processpublish
Publish directory, in our case, it isbuild
as CreateReactApp builds to this directory. It may differ for Next.js or Gatsby or any other site buildercommand
is to launch the build command for your project. It can be any CLI command, usually,npm
script in FE Project andpackage.json
contains a more detailed command for buildfunctions
The Functions directory for Netlify Functions to build. Usually, it isfunctions
but you can choose anything you want
-
[[redirects]]
a directive to redirect requests from one endpoint to otherfrom
This is the Incoming request URL patternto
Where to redirect the request tostatus
(optional) status code you wanna send with redirect
-
:splat
placeholder holding the value for*
match infrom
You can read more about the netlify config file here:
You can see the demo and repository from the following links:
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 @heypankaj_ 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