Lesson 6.4: Practice - GitHub API Integration
Duration: 60 minutes
Learning Objectives
By the end of this lesson, you will be able to:
- Set up a GitHub Personal Access Token
- Create a typed client for the GitHub API
- Handle rate limiting and pagination
- Build a complete CLI tool that interacts with GitHub
- 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:
- Go to github.com/settings/tokens
- Click "Generate new token" (classic)
- Give it a descriptive name (e.g., "Course Practice Token")
- Select scopes:
read:user- Read user profileuser:email- Access email addressespublic_repo- Access public repositories
- Click "Generate token"
- 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
- Personal Access Tokens are the simplest way to authenticate with GitHub
- Always store tokens in environment variables, never in code
- Create a typed API client for better developer experience
- Handle rate limits by checking headers and implementing retry logic
- Use pagination helpers for endpoints that return many results
- Provide helpful error messages that guide users to solutions
- 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!