From Zero to AI

Lesson 6.4: Practice - GitHub API Integration

Duration: 60 minutes

Learning Objectives

By the end of this lesson, you will be able to:

  1. Set up a GitHub Personal Access Token
  2. Create a typed client for the GitHub API
  3. Handle rate limiting and pagination
  4. Build a complete CLI tool that interacts with GitHub
  5. Apply authentication best practices in a real project

Introduction

In this practical lesson, we will build a complete GitHub API client using everything we have learned about authentication. We will create a command-line tool that can:

  • Fetch your profile information
  • List your repositories
  • Search for repositories by topic
  • Handle authentication errors gracefully
  • Respect rate limits

This is a hands-on project that brings together API keys, Bearer tokens, and best practices.


Setting Up GitHub Authentication

Creating a Personal Access Token

GitHub Personal Access Tokens (PATs) are the simplest way to authenticate with the GitHub API.

Steps to create a token:

  1. Go to github.com/settings/tokens
  2. Click "Generate new token" (classic)
  3. Give it a descriptive name (e.g., "Course Practice Token")
  4. Select scopes:
    • read:user - Read user profile
    • user:email - Access email addresses
    • public_repo - Access public repositories
  5. Click "Generate token"
  6. Copy the token immediately - you will not see it again!

Storing the Token

Create a .env file in your project:

# .env
GITHUB_TOKEN=ghp_your_token_here

Add to .gitignore:

.env
.env.local

Project Setup

Initialize the Project

mkdir github-cli
cd github-cli
npm init -y
npm install dotenv
npm install -D typescript @types/node
npx tsc --init

TypeScript Configuration

Update tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

Project Structure

github-cli/
├── src/
│   ├── index.ts           # Main entry point
│   ├── github-client.ts   # API client
│   ├── types.ts           # Type definitions
│   └── utils.ts           # Helper functions
├── .env
├── .gitignore
├── package.json
└── tsconfig.json

Building the GitHub Client

Type Definitions

Create src/types.ts:

// GitHub User
export interface GitHubUser {
  login: string;
  id: number;
  avatar_url: string;
  name: string | null;
  company: string | null;
  blog: string;
  location: string | null;
  email: string | null;
  bio: string | null;
  public_repos: number;
  followers: number;
  following: number;
  created_at: string;
}

// GitHub Repository
export interface GitHubRepository {
  id: number;
  name: string;
  full_name: string;
  private: boolean;
  owner: {
    login: string;
    avatar_url: string;
  };
  html_url: string;
  description: string | null;
  fork: boolean;
  language: string | null;
  stargazers_count: number;
  watchers_count: number;
  forks_count: number;
  open_issues_count: number;
  default_branch: string;
  created_at: string;
  updated_at: string;
  pushed_at: string;
}

// GitHub Search Results
export interface GitHubSearchResult<T> {
  total_count: number;
  incomplete_results: boolean;
  items: T[];
}

// Rate Limit Information
export interface RateLimitInfo {
  limit: number;
  remaining: number;
  reset: Date;
  used: number;
}

// API Error Response
export interface GitHubErrorResponse {
  message: string;
  documentation_url?: string;
}

// Client Options
export interface GitHubClientOptions {
  token: string;
  userAgent?: string;
}

Utility Functions

Create src/utils.ts:

import { RateLimitInfo } from './types';

/**
 * Extract rate limit info from response headers
 */
export function extractRateLimitInfo(headers: Headers): RateLimitInfo {
  return {
    limit: parseInt(headers.get('X-RateLimit-Limit') || '60', 10),
    remaining: parseInt(headers.get('X-RateLimit-Remaining') || '0', 10),
    reset: new Date(parseInt(headers.get('X-RateLimit-Reset') || '0', 10) * 1000),
    used: parseInt(headers.get('X-RateLimit-Used') || '0', 10),
  };
}

/**
 * Format a date for display
 */
export function formatDate(dateString: string): string {
  const date = new Date(dateString);
  return date.toLocaleDateString('en-US', {
    year: 'numeric',
    month: 'short',
    day: 'numeric',
  });
}

/**
 * Format a number with K/M suffixes
 */
export function formatNumber(num: number): string {
  if (num >= 1000000) {
    return (num / 1000000).toFixed(1) + 'M';
  }
  if (num >= 1000) {
    return (num / 1000).toFixed(1) + 'K';
  }
  return num.toString();
}

/**
 * Sleep for a given number of milliseconds
 */
export function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

/**
 * Parse Link header for pagination
 */
export function parseLinkHeader(header: string | null): Record<string, string> {
  if (!header) return {};

  const links: Record<string, string> = {};
  const parts = header.split(',');

  for (const part of parts) {
    const match = part.match(/<([^>]+)>;\s*rel="([^"]+)"/);
    if (match) {
      links[match[2]] = match[1];
    }
  }

  return links;
}

The GitHub Client

Create src/github-client.ts:

import {
  GitHubClientOptions,
  GitHubErrorResponse,
  GitHubRepository,
  GitHubSearchResult,
  GitHubUser,
  RateLimitInfo,
} from './types';
import { extractRateLimitInfo, parseLinkHeader, sleep } from './utils';

export class GitHubApiError extends Error {
  constructor(
    message: string,
    public status: number,
    public rateLimitInfo?: RateLimitInfo
  ) {
    super(message);
    this.name = 'GitHubApiError';
  }
}

export class GitHubClient {
  private token: string;
  private userAgent: string;
  private baseUrl = 'https://api.github.com';
  private lastRateLimitInfo: RateLimitInfo | null = null;

  constructor(options: GitHubClientOptions) {
    this.token = options.token;
    this.userAgent = options.userAgent || 'TypeScript-GitHub-Client';
  }

  /**
   * Get headers for API requests
   */
  private getHeaders(): Record<string, string> {
    return {
      Authorization: `Bearer ${this.token}`,
      Accept: 'application/vnd.github.v3+json',
      'User-Agent': this.userAgent,
      'X-GitHub-Api-Version': '2022-11-28',
    };
  }

  /**
   * Make an authenticated request to the GitHub API
   */
  private async request<T>(
    endpoint: string,
    options: RequestInit = {}
  ): Promise<{ data: T; rateLimit: RateLimitInfo; headers: Headers }> {
    const url = endpoint.startsWith('http') ? endpoint : `${this.baseUrl}${endpoint}`;

    const response = await fetch(url, {
      ...options,
      headers: {
        ...this.getHeaders(),
        ...options.headers,
      },
    });

    const rateLimit = extractRateLimitInfo(response.headers);
    this.lastRateLimitInfo = rateLimit;

    // Handle rate limiting
    if (response.status === 403 && rateLimit.remaining === 0) {
      const waitTime = rateLimit.reset.getTime() - Date.now();
      throw new GitHubApiError(
        `Rate limit exceeded. Resets at ${rateLimit.reset.toLocaleTimeString()}`,
        403,
        rateLimit
      );
    }

    // Handle authentication errors
    if (response.status === 401) {
      throw new GitHubApiError('Authentication failed. Please check your token.', 401, rateLimit);
    }

    // Handle not found
    if (response.status === 404) {
      throw new GitHubApiError('Resource not found.', 404, rateLimit);
    }

    // Handle other errors
    if (!response.ok) {
      let errorMessage = `GitHub API error: ${response.status}`;
      try {
        const errorBody: GitHubErrorResponse = await response.json();
        errorMessage = errorBody.message;
      } catch {
        // Could not parse error body
      }
      throw new GitHubApiError(errorMessage, response.status, rateLimit);
    }

    const data = (await response.json()) as T;
    return { data, rateLimit, headers: response.headers };
  }

  /**
   * Get the current rate limit status
   */
  getRateLimitInfo(): RateLimitInfo | null {
    return this.lastRateLimitInfo;
  }

  /**
   * Get the authenticated user's profile
   */
  async getCurrentUser(): Promise<GitHubUser> {
    const { data } = await this.request<GitHubUser>('/user');
    return data;
  }

  /**
   * Get a user by username
   */
  async getUser(username: string): Promise<GitHubUser> {
    const { data } = await this.request<GitHubUser>(`/users/${username}`);
    return data;
  }

  /**
   * List repositories for the authenticated user
   */
  async listMyRepositories(
    options: {
      sort?: 'created' | 'updated' | 'pushed' | 'full_name';
      direction?: 'asc' | 'desc';
      perPage?: number;
      page?: number;
    } = {}
  ): Promise<GitHubRepository[]> {
    const params = new URLSearchParams();
    if (options.sort) params.set('sort', options.sort);
    if (options.direction) params.set('direction', options.direction);
    params.set('per_page', (options.perPage || 30).toString());
    params.set('page', (options.page || 1).toString());

    const { data } = await this.request<GitHubRepository[]>(`/user/repos?${params.toString()}`);
    return data;
  }

  /**
   * List repositories for a specific user
   */
  async listUserRepositories(
    username: string,
    options: {
      sort?: 'created' | 'updated' | 'pushed' | 'full_name';
      perPage?: number;
    } = {}
  ): Promise<GitHubRepository[]> {
    const params = new URLSearchParams();
    if (options.sort) params.set('sort', options.sort);
    params.set('per_page', (options.perPage || 30).toString());

    const { data } = await this.request<GitHubRepository[]>(
      `/users/${username}/repos?${params.toString()}`
    );
    return data;
  }

  /**
   * Get a specific repository
   */
  async getRepository(owner: string, repo: string): Promise<GitHubRepository> {
    const { data } = await this.request<GitHubRepository>(`/repos/${owner}/${repo}`);
    return data;
  }

  /**
   * Search for repositories
   */
  async searchRepositories(
    query: string,
    options: {
      sort?: 'stars' | 'forks' | 'help-wanted-issues' | 'updated';
      order?: 'asc' | 'desc';
      perPage?: number;
      page?: number;
    } = {}
  ): Promise<GitHubSearchResult<GitHubRepository>> {
    const params = new URLSearchParams();
    params.set('q', query);
    if (options.sort) params.set('sort', options.sort);
    if (options.order) params.set('order', options.order);
    params.set('per_page', (options.perPage || 30).toString());
    params.set('page', (options.page || 1).toString());

    const { data } = await this.request<GitHubSearchResult<GitHubRepository>>(
      `/search/repositories?${params.toString()}`
    );
    return data;
  }

  /**
   * Fetch all pages of a paginated endpoint
   */
  async fetchAllPages<T>(endpoint: string, maxPages: number = 10): Promise<T[]> {
    const allItems: T[] = [];
    let url: string | undefined = `${this.baseUrl}${endpoint}`;
    let page = 0;

    while (url && page < maxPages) {
      const response = await fetch(url, {
        headers: this.getHeaders(),
      });

      if (!response.ok) {
        throw new GitHubApiError(`Failed to fetch page ${page + 1}`, response.status);
      }

      const data = (await response.json()) as T[];
      allItems.push(...data);

      // Get next page URL from Link header
      const links = parseLinkHeader(response.headers.get('Link'));
      url = links.next;
      page++;

      // Small delay to be nice to the API
      if (url) {
        await sleep(100);
      }
    }

    return allItems;
  }
}

Building the CLI Application

Create src/index.ts:

import 'dotenv/config';

import { GitHubApiError, GitHubClient } from './github-client';
import { formatDate, formatNumber } from './utils';

// Ensure token is available
const token = process.env.GITHUB_TOKEN;

if (!token) {
  console.error('Error: GITHUB_TOKEN environment variable is not set.');
  console.error('Please create a .env file with your GitHub token:');
  console.error('  GITHUB_TOKEN=ghp_your_token_here');
  process.exit(1);
}

// Create client instance
const github = new GitHubClient({ token });

/**
 * Display user profile information
 */
async function showProfile(username?: string): Promise<void> {
  console.log('\n--- User Profile ---\n');

  const user = username ? await github.getUser(username) : await github.getCurrentUser();

  console.log(`Username: ${user.login}`);
  console.log(`Name: ${user.name || '(not set)'}`);
  console.log(`Bio: ${user.bio || '(not set)'}`);
  console.log(`Location: ${user.location || '(not set)'}`);
  console.log(`Company: ${user.company || '(not set)'}`);
  console.log(`Blog: ${user.blog || '(not set)'}`);
  console.log(`Public Repos: ${user.public_repos}`);
  console.log(`Followers: ${formatNumber(user.followers)}`);
  console.log(`Following: ${formatNumber(user.following)}`);
  console.log(`Member since: ${formatDate(user.created_at)}`);
}

/**
 * Display repository list
 */
async function showRepositories(username?: string, limit: number = 10): Promise<void> {
  console.log('\n--- Repositories ---\n');

  const repos = username
    ? await github.listUserRepositories(username, {
        sort: 'updated',
        perPage: limit,
      })
    : await github.listMyRepositories({
        sort: 'updated',
        perPage: limit,
      });

  if (repos.length === 0) {
    console.log('No repositories found.');
    return;
  }

  for (const repo of repos) {
    const stars = formatNumber(repo.stargazers_count);
    const forks = formatNumber(repo.forks_count);
    const lang = repo.language || 'Unknown';
    const visibility = repo.private ? 'Private' : 'Public';

    console.log(`${repo.name}`);
    console.log(`  ${repo.description || '(no description)'}`);
    console.log(`  Language: ${lang} | Stars: ${stars} | Forks: ${forks} | ${visibility}`);
    console.log(`  Updated: ${formatDate(repo.updated_at)}`);
    console.log();
  }
}

/**
 * Search for repositories
 */
async function searchRepos(query: string, limit: number = 10): Promise<void> {
  console.log(`\n--- Search Results for "${query}" ---\n`);

  const results = await github.searchRepositories(query, {
    sort: 'stars',
    order: 'desc',
    perPage: limit,
  });

  console.log(`Found ${formatNumber(results.total_count)} repositories\n`);

  for (const repo of results.items) {
    const stars = formatNumber(repo.stargazers_count);
    const forks = formatNumber(repo.forks_count);

    console.log(`${repo.full_name}`);
    console.log(`  ${repo.description || '(no description)'}`);
    console.log(`  Stars: ${stars} | Forks: ${forks} | Language: ${repo.language || 'N/A'}`);
    console.log(`  URL: ${repo.html_url}`);
    console.log();
  }
}

/**
 * Show rate limit status
 */
function showRateLimit(): void {
  const rateLimit = github.getRateLimitInfo();

  if (rateLimit) {
    console.log('\n--- Rate Limit Status ---\n');
    console.log(`Limit: ${rateLimit.limit}`);
    console.log(`Remaining: ${rateLimit.remaining}`);
    console.log(`Used: ${rateLimit.used}`);
    console.log(`Resets at: ${rateLimit.reset.toLocaleTimeString()}`);
  }
}

/**
 * Display help information
 */
function showHelp(): void {
  console.log(`
GitHub CLI Tool

Usage:
  npm start -- <command> [options]

Commands:
  profile [username]     Show user profile (default: authenticated user)
  repos [username]       List repositories (default: authenticated user)
  search <query>         Search for repositories
  help                   Show this help message

Examples:
  npm start -- profile
  npm start -- profile octocat
  npm start -- repos
  npm start -- repos microsoft
  npm start -- search "typescript tutorial"
`);
}

/**
 * Main entry point
 */
async function main(): Promise<void> {
  const args = process.argv.slice(2);
  const command = args[0] || 'help';

  try {
    switch (command.toLowerCase()) {
      case 'profile':
        await showProfile(args[1]);
        break;

      case 'repos':
      case 'repositories':
        await showRepositories(args[1]);
        break;

      case 'search':
        if (!args[1]) {
          console.error('Error: Search query required');
          console.error('Usage: npm start -- search <query>');
          process.exit(1);
        }
        await searchRepos(args.slice(1).join(' '));
        break;

      case 'help':
      case '--help':
      case '-h':
        showHelp();
        break;

      default:
        console.error(`Unknown command: ${command}`);
        showHelp();
        process.exit(1);
    }

    showRateLimit();
  } catch (error) {
    if (error instanceof GitHubApiError) {
      console.error(`\nGitHub API Error: ${error.message}`);

      if (error.status === 401) {
        console.error('Please check that your GITHUB_TOKEN is valid.');
      }

      if (error.rateLimitInfo) {
        console.error(`Rate limit: ${error.rateLimitInfo.remaining}/${error.rateLimitInfo.limit}`);
      }
    } else {
      console.error('\nUnexpected error:', error);
    }
    process.exit(1);
  }
}

main();

Update package.json

Add scripts to package.json:

{
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js",
    "dev": "tsc && node dist/index.js"
  }
}

Running the Application

Build and Run

# Build TypeScript
npm run build

# Run commands
npm start -- profile
npm start -- profile octocat
npm start -- repos
npm start -- search "typescript tutorial"

Example Output

--- User Profile ---

Username: yourusername
Name: Your Name
Bio: Software Developer
Location: London, UK
Company: @mycompany
Blog: https://yourblog.com
Public Repos: 42
Followers: 150
Following: 89
Member since: Jan 15, 2018

--- Rate Limit Status ---

Limit: 5000
Remaining: 4998
Used: 2
Resets at: 3:45:00 PM

Exercises

Exercise 1: Add Starred Repositories

Add a command to list repositories the user has starred:

// Add to github-client.ts
async listStarredRepositories(options?: {
  sort?: "created" | "updated";
  perPage?: number;
}): Promise<GitHubRepository[]> {
  // Your implementation
}
Solution

Add to github-client.ts:

/**
 * List repositories starred by the authenticated user
 */
async listStarredRepositories(options: {
  sort?: "created" | "updated";
  direction?: "asc" | "desc";
  perPage?: number;
  page?: number;
} = {}): Promise<GitHubRepository[]> {
  const params = new URLSearchParams();
  if (options.sort) params.set("sort", options.sort);
  if (options.direction) params.set("direction", options.direction);
  params.set("per_page", (options.perPage || 30).toString());
  params.set("page", (options.page || 1).toString());

  const { data } = await this.request<GitHubRepository[]>(
    `/user/starred?${params.toString()}`
  );
  return data;
}

Add to index.ts:

async function showStarredRepos(limit: number = 10): Promise<void> {
  console.log("\n--- Starred Repositories ---\n");

  const repos = await github.listStarredRepositories({
    sort: "updated",
    perPage: limit
  });

  if (repos.length === 0) {
    console.log("No starred repositories found.");
    return;
  }

  for (const repo of repos) {
    console.log(`${repo.full_name}`);
    console.log(`  ${repo.description || "(no description)"}`);
    console.log(`  Stars: ${formatNumber(repo.stargazers_count)} | Language: ${repo.language || "N/A"}`);
    console.log();
  }
}

// Add case in main():
case "starred":
case "stars":
  await showStarredRepos();
  break;

Exercise 2: Add Repository Details

Add a command to show detailed information about a specific repository:

// Usage: npm start -- repo owner/name
Solution

Add to index.ts:

async function showRepoDetails(fullName: string): Promise<void> {
  const [owner, name] = fullName.split("/");

  if (!owner || !name) {
    throw new Error("Invalid repository format. Use: owner/repo");
  }

  console.log(`\n--- Repository: ${fullName} ---\n`);

  const repo = await github.getRepository(owner, name);

  console.log(`Name: ${repo.name}`);
  console.log(`Full Name: ${repo.full_name}`);
  console.log(`Description: ${repo.description || "(none)"}`);
  console.log(`URL: ${repo.html_url}`);
  console.log(`\nStatistics:`);
  console.log(`  Stars: ${formatNumber(repo.stargazers_count)}`);
  console.log(`  Forks: ${formatNumber(repo.forks_count)}`);
  console.log(`  Watchers: ${formatNumber(repo.watchers_count)}`);
  console.log(`  Open Issues: ${repo.open_issues_count}`);
  console.log(`\nDetails:`);
  console.log(`  Language: ${repo.language || "Not specified"}`);
  console.log(`  Default Branch: ${repo.default_branch}`);
  console.log(`  Private: ${repo.private ? "Yes" : "No"}`);
  console.log(`  Fork: ${repo.fork ? "Yes" : "No"}`);
  console.log(`\nDates:`);
  console.log(`  Created: ${formatDate(repo.created_at)}`);
  console.log(`  Updated: ${formatDate(repo.updated_at)}`);
  console.log(`  Last Push: ${formatDate(repo.pushed_at)}`);
}

// Add case in main():
case "repo":
case "repository":
  if (!args[1]) {
    console.error("Error: Repository name required (format: owner/repo)");
    process.exit(1);
  }
  await showRepoDetails(args[1]);
  break;

Exercise 3: Add Rate Limit Retry

Modify the client to automatically wait and retry when rate limited:

// Should wait for rate limit reset and retry automatically
Solution

Modify request method in github-client.ts:

private async request<T>(
  endpoint: string,
  options: RequestInit = {},
  retryOnRateLimit: boolean = true
): Promise<{ data: T; rateLimit: RateLimitInfo; headers: Headers }> {
  const url = endpoint.startsWith("http")
    ? endpoint
    : `${this.baseUrl}${endpoint}`;

  const response = await fetch(url, {
    ...options,
    headers: {
      ...this.getHeaders(),
      ...options.headers
    }
  });

  const rateLimit = extractRateLimitInfo(response.headers);
  this.lastRateLimitInfo = rateLimit;

  // Handle rate limiting with retry
  if (response.status === 403 && rateLimit.remaining === 0) {
    if (retryOnRateLimit) {
      const waitTime = Math.max(0, rateLimit.reset.getTime() - Date.now());

      if (waitTime <= 60000) { // Only wait if less than 1 minute
        console.log(`Rate limited. Waiting ${Math.ceil(waitTime / 1000)}s...`);
        await sleep(waitTime + 1000); // Add 1 second buffer

        // Retry the request (without retry flag to prevent infinite loop)
        return this.request<T>(endpoint, options, false);
      }
    }

    throw new GitHubApiError(
      `Rate limit exceeded. Resets at ${rateLimit.reset.toLocaleTimeString()}`,
      403,
      rateLimit
    );
  }

  // ... rest of error handling
  if (response.status === 401) {
    throw new GitHubApiError(
      "Authentication failed. Please check your token.",
      401,
      rateLimit
    );
  }

  if (response.status === 404) {
    throw new GitHubApiError(
      "Resource not found.",
      404,
      rateLimit
    );
  }

  if (!response.ok) {
    let errorMessage = `GitHub API error: ${response.status}`;
    try {
      const errorBody: GitHubErrorResponse = await response.json();
      errorMessage = errorBody.message;
    } catch {
      // Could not parse error body
    }
    throw new GitHubApiError(errorMessage, response.status, rateLimit);
  }

  const data = await response.json() as T;
  return { data, rateLimit, headers: response.headers };
}

Exercise 4: Add Gist Support

Add support for listing and creating gists:

interface Gist {
  id: string;
  html_url: string;
  description: string | null;
  public: boolean;
  files: Record<string, { filename: string; type: string; size: number }>;
  created_at: string;
}

// Add methods to list gists and create a new gist
Solution

Add types to types.ts:

export interface GistFile {
  filename: string;
  type: string;
  language: string | null;
  raw_url: string;
  size: number;
  content?: string;
}

export interface Gist {
  id: string;
  html_url: string;
  description: string | null;
  public: boolean;
  files: Record<string, GistFile>;
  created_at: string;
  updated_at: string;
}

export interface CreateGistRequest {
  description?: string;
  public?: boolean;
  files: Record<string, { content: string }>;
}

Add to github-client.ts:

import { Gist, CreateGistRequest } from "./types";

/**
 * List gists for the authenticated user
 */
async listMyGists(options: {
  perPage?: number;
  page?: number;
} = {}): Promise<Gist[]> {
  const params = new URLSearchParams();
  params.set("per_page", (options.perPage || 30).toString());
  params.set("page", (options.page || 1).toString());

  const { data } = await this.request<Gist[]>(
    `/gists?${params.toString()}`
  );
  return data;
}

/**
 * Create a new gist
 */
async createGist(gist: CreateGistRequest): Promise<Gist> {
  const { data } = await this.request<Gist>("/gists", {
    method: "POST",
    body: JSON.stringify(gist)
  });
  return data;
}

/**
 * Get a specific gist
 */
async getGist(gistId: string): Promise<Gist> {
  const { data } = await this.request<Gist>(`/gists/${gistId}`);
  return data;
}

Add to index.ts:

async function showGists(limit: number = 10): Promise<void> {
  console.log("\n--- Your Gists ---\n");

  const gists = await github.listMyGists({ perPage: limit });

  if (gists.length === 0) {
    console.log("No gists found.");
    return;
  }

  for (const gist of gists) {
    const fileCount = Object.keys(gist.files).length;
    const fileNames = Object.keys(gist.files).slice(0, 3).join(", ");
    const visibility = gist.public ? "Public" : "Secret";

    console.log(`${gist.description || "(no description)"}`);
    console.log(`  Files: ${fileNames}${fileCount > 3 ? ` (+${fileCount - 3} more)` : ""}`);
    console.log(`  ${visibility} | Created: ${formatDate(gist.created_at)}`);
    console.log(`  URL: ${gist.html_url}`);
    console.log();
  }
}

// Add case in main():
case "gists":
  await showGists();
  break;

Key Takeaways

  1. Personal Access Tokens are the simplest way to authenticate with GitHub
  2. Always store tokens in environment variables, never in code
  3. Create a typed API client for better developer experience
  4. Handle rate limits by checking headers and implementing retry logic
  5. Use pagination helpers for endpoints that return many results
  6. Provide helpful error messages that guide users to solutions
  7. Structure your code with separation of concerns (client, types, utilities)

Resources

Resource Type Level
GitHub REST API Docs Documentation Beginner
GitHub Personal Access Tokens Documentation Beginner
GitHub Rate Limiting Documentation Intermediate
Octokit.js Library Intermediate

Module Complete

Congratulations! You have completed the Authentication module. You now understand:

  • How API keys work and how to use them securely
  • Bearer tokens and JWT structure
  • OAuth 2.0 concepts and flows
  • Building authenticated API clients with TypeScript

Continue your learning journey with the next course!

Back to Course 3 Overview