👨‍💻

【Next.js】GithubのAPIを使って使用技術スタックを表示した話

2025/02/12に公開

概要

「自分のポートフォリオでも作ろうかなー^^」と思い始めてまず一番最初に書かなければいけないと思ったこと、それは自分がどんな技術を使用しているのか、ということでした。しかし、現在使用している技術スタックは更新されていくものです。それをいちいちサイトを編集して変更する。というのはあまりにも非効率的だと思いました。(皆さんもそう思いませんか??)

そこでGithubの「Language」の欄を利用してリポジトリで使用されたプログラミング言語を取得してくることで、自分の使用している技術スタックを更新し続けることができるのではないかと考えたのです。


hereで示された部分で使用技術スタックが示されている

画像では1リポジトリの使用技術ですが、これをまとめて自分の全リポジトリの使用技術スタックを取得して表示することを目指しました。

実装

Personal access tokensの取得

まずはGithubからデータを取得するために自分のGithubアカウントからTokenを取得する必要があります。
↓ こちら参照して取得しましょう
https://docs.github.com/ja/enterprise-cloud@latest/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens

こちらと合わせて、GITHUB_USERNAMEを環境変数として.envファイルに定義しておきます。

.env
GITHUB_USERNAME="自分のユーザーネーム"
GITHUB_TOKEN="自分のアクセストークン"

APIの実装

Next.jsでAPIを作成します。

1. 環境変数とAPIエンドポイントの設定

app/api/skills/route.ts
const GITHUB_USERNAME = process.env.GITHUB_USERNAME;
const GITHUB_TOKEN = process.env.GITHUB_TOKEN; // サーバー専用のトークン
const GITHUB_API_URL = `https://api.github.com/users/${GITHUB_USERNAME}/repos`;

GitHubのユーザー名とアクセストークンを環境変数から取得し、ユーザーのリポジトリ一覧を取得するためのGitHub APIエンドポイントを構築しています。

2. APIルートのエントリーポイント

app/api/skills/route.ts
export async function GET() {
  try {
    // GitHub APIからリポジトリ一覧を取得
    const reposResponse = await fetch(GITHUB_API_URL, {
      headers: {
        Authorization: `token ${GITHUB_TOKEN}`,
      },
    });
    if (!reposResponse.ok) {
      return NextResponse.json(
        { error: `GitHub API Error: ${reposResponse.statusText}` },
        { status: reposResponse.status }
      );
    }
    const repos = await reposResponse.json();
    // ・・・
  } catch (error) {
    console.error("Error fetching GitHub languages:", error);
    return NextResponse.json({ error: "Internal Server Error" }, { status: 500 });
  }
}
  • GET関数: この関数はHTTP GETリクエストを処理するエンドポイントのエントリーポイントです。Next.jsの新しいApp RouterにおけるAPI Routesの実装例です。

  • try-catch構造: 全体をtry-catchで囲むことで、予期しないエラーが発生した場合にも適切なエラーレスポンス(500)を返すようにしています。

  • リポジトリの取得: fetch関数を使い、設定したGitHub APIエンドポイントからユーザーのリポジトリ一覧を取得します。ヘッダーにアクセストークンを含めることで、認証済みのリクエストを行っています。レスポンスが正常でない場合、エラー内容(statusTextなど)をJSON形式で返却します。

3. 各リポジトリの言語データ取得と集計

app/api/skills/route.ts
const languageStats: Record<string, number> = {};
let totalBytes = 0;

// 各リポジトリについて言語データを取得
for (const repo of repos) {
  if (!repo.fork && repo.languages_url) {
    try {
      const langResponse = await fetch(repo.languages_url, {
        headers: {
          Authorization: `token ${GITHUB_TOKEN}`,
        },
      });
      if (!langResponse.ok) {
        console.warn(`Languages API Error for ${repo.name}: ${langResponse.statusText}`);
        continue;
      }
      const langData = await langResponse.json();

      for (const [lang, bytes] of Object.entries(langData) as [string, number][]) {
        languageStats[lang] = (languageStats[lang] || 0) + bytes;
        totalBytes += bytes;
      }
    } catch (langError) {
      console.warn(`Failed to fetch languages for repo: ${repo.name}`, langError);
    }
  }
}
  • 対象のリポジトリ選別: if (!repo.fork && repo.languages_url)の条件で、forkされたリポジトリを除外しています。これは、fork先では元のプロジェクトのコードが含まれるため、オリジナルの言語比率を知りたい場合に除外するケースが多いためです。
    また、languages_urlプロパティが存在するリポジトリのみ処理対象としています。

  • 言語データの取得: 各リポジトリについて、GitHubが提供するlanguages_urlを使い、そのリポジトリ内の各プログラミング言語ごとのコード量(バイト数)を取得します。
    ここでもアクセストークンを用いて認証済みのリクエストを行っています。
    もしリクエストが失敗した場合は、console.warnで警告を出し、そのリポジトリの処理をスキップします。

  • データ集計: 取得した langDataは、たとえば { "JavaScript": 15000, "CSS": 5000 } のような形で返ってきます。
    languageStatsオブジェクトを使い、全リポジトリ横断で各言語のバイト数を累積していきます。
    totalBytesには、全言語のバイト数の合計が加算され、後で割合計算に使用されます。

4. 集計結果の後処理とレスポンス生成

app/api/skills/route.ts
if (totalBytes === 0) {
  return NextResponse.json({ error: "No language data found" }, { status: 404 });
}

const skillsData = Object.entries(languageStats)
  .map(([name, bytes]) => ({
    name,
    level: Math.round((bytes / totalBytes) * 100),
  }))
  .sort((a, b) => b.level - a.level)
  .slice(0, 10);

return NextResponse.json(skillsData, { status: 200 });
  • 全データが0の場合のハンドリング: もし全リポジトリから言語データが取得できず、totalBytes が0の場合は、404エラーを返して「言語データが見つからなかった」という旨のエラーレスポンスを出します。

  • データの整形: Object.entries(languageStats) で各言語とその累積バイト数のペアを取得し、配列に変換します。
    各ペアに対して、bytes / totalBytes * 100 の計算を行い、その言語が全体に対してどれだけの割合(パーセンテージ)を占めるかを求めています。結果は四捨五入して整数値(level)にしています。

  • ソートと上位10言語の選別: 計算結果の配列を level の降順にソートし、slice(0, 10) によって上位10件のみを抽出しています。これにより、最も使用されている(=得意な)言語上位10件を返す意図となっています。

  • レスポンス生成: NextResponse.json を使って、整形済みの skillsData をJSON形式で返却し、HTTPステータス200(成功)とともにクライアントにレスポンスを送ります。

5. エラーハンドリング全般

GitHub APIへのリクエスト時のエラーチェック: リポジトリ一覧取得時、言語データ取得時の両方で、response.ok をチェックしており、問題があれば適切にエラーレスポンスまたは警告ログを出力しています。
try-catchで全体を保護: 外側のtry-catchにより、予期しない例外が発生した場合でも、500 Internal Server Error を返すように設計されています。これにより、エンドポイントがクラッシュせずにエラー状況をクライアントに伝えることができます。

実装全体

以下のコードが実装したAPIのプログラム全体になります。

app/api/skills/route.ts
// app/api/skills/route.ts
import { NextResponse } from "next/server";

const GITHUB_USERNAME = process.env.GITHUB_USERNAME;
const GITHUB_TOKEN = process.env.GITHUB_TOKEN; // サーバー専用のトークン
const GITHUB_API_URL = `https://api.github.com/users/${GITHUB_USERNAME}/repos`;

export async function GET() {
  try {
    const reposResponse = await fetch(GITHUB_API_URL, {
      headers: {
        Authorization: `token ${GITHUB_TOKEN}`,
      },
    });
    if (!reposResponse.ok) {
      return NextResponse.json(
        { error: `GitHub API Error: ${reposResponse.statusText}` },
        { status: reposResponse.status }
      );
    }
    const repos = await reposResponse.json();

    const languageStats: Record<string, number> = {};
    let totalBytes = 0;

    // 各リポジトリについて言語データを取得
    for (const repo of repos) {
      if (!repo.fork && repo.languages_url) {
        try {
          const langResponse = await fetch(repo.languages_url, {
            headers: {
              Authorization: `token ${GITHUB_TOKEN}`,
            },
          });
          if (!langResponse.ok) {
            console.warn(`Languages API Error for ${repo.name}: ${langResponse.statusText}`);
            continue;
          }
          const langData = await langResponse.json();

          for (const [lang, bytes] of Object.entries(langData) as [string, number][]) {
            languageStats[lang] = (languageStats[lang] || 0) + bytes;
            totalBytes += bytes;
          }
        } catch (langError) {
          console.warn(`Failed to fetch languages for repo: ${repo.name}`, langError);
        }
      }
    }

    if (totalBytes === 0) {
      return NextResponse.json({ error: "No language data found" }, { status: 404 });
    }

    const skillsData = Object.entries(languageStats)
      .map(([name, bytes]) => ({
        name,
        level: Math.round((bytes / totalBytes) * 100),
      }))
      .sort((a, b) => b.level - a.level)
      .slice(0, 10);

    return NextResponse.json(skillsData, { status: 200 });
  } catch (error) {
    console.error("Error fetching GitHub languages:", error);
    return NextResponse.json({ error: "Internal Server Error" }, { status: 500 });
  }
}

このAPIをフロントで呼び出し、使用技術スタックとして取得した言語たちを表示しています。

app/components/Skills.tsx
interface Skill {
  name: string;
  level: number;
}

・・・

const Skills = () => {
  const [skills, setSkills] = useState<Skill[]>([]);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const fetchSkills = async () => {
      try {
        const response = await fetch("/api/skills");
        if (!response.ok) {
          throw new Error(`API Error: ${response.statusText}`);
        }
        const data: Skill[] = await response.json();
        setSkills(data);
        setError(null);
      } catch (err) {
        console.error("GitHub API の取得に失敗しました:", err);
      }
    };

    fetchSkills();
  }, []);

・・・

実際にどのように表示しているかは私のポートフォリオの「Skills」セクションでぜひ確認してもらいたいです!笑

まとめ

今回はGithubのAPIを使って自分のリポジトリで使用されている言語から自分の主要な技術スタックを取得し表示しました!
自分のポートフォリオを作ることもささりますが、なにより「APIをつかって」「Githubのリポジトリから」自分の技術スタックを表示している、というのがWebエンジニア界隈にかなり刺さる内容だと思うので、いまから就活を考えるエンジニア志望の学生にぜひ使ってもらいたいと思います!!

Discussion