📜

GitHub GraphQL APIで掲示板作ってみた

2022/12/02に公開

はじめに

Xbit Advent Calendar 2022の2日目を担当する高橋です。
最近はらくしふ労務管理というプロダクトで主にフロントエンド開発をしています。
趣味はフットサルとゲームです。

この記事ではログイン不要の匿名掲示板を作って実際にWebに公開します。
GitHubとVercelのアカウントだけあれば大丈夫です。

成果物

GitHub: https://github.com/kthatoto/github-issues-message-boards
実物: https://message-boards.vercel.app

掲示板一覧 掲示板

概要

GitHubのissue機能を掲示板に見立てることでバックエンドの実装を省略してVercelで楽にホスティングして、ほぼほぼフロントエンドの開発だけで掲示板サービスを作ります

前半はNextアプリケーションを生成、VercelでホスティングしてGitHubのGraphQL APIを利用するところまで具体的な手順を解説します。
後半は出来上がったコードの要所をつまみながら解説していきます。(3分クッキングのように)

使用技術/サービス

実装する機能

GitHubにはrepositoryを掲示板群としてそれに紐づくissueという形で掲示板を作成することができcommentを投稿することができる機能があります。
すでに掲示板サービスとしての最低限の要件を満たしているのでそのまま使っていきます。

  • 掲示板一覧ページ
    • 掲示板一覧取得 → issue一覧取得
    • 掲示板作成 → issue作成
  • 掲示板詳細ページ(コメント一覧)
    • 掲示板コメント一覧取得 → issue/comment一覧取得
    • 掲示板コメント投稿 → issue/comment投稿

これらをGitHub GraphQL API経由で行います。
GitHub GraphQL APIを利用するのにPersonal Access Tokenが必要なのですが、そのトークンの発行者名義でissueの作成,commentの投稿が行われます。

1. Nextを立ち上げる

初期設定

$ yarn create next-app
yarn create v1.22.19
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
[4/4] 🔨  Building fresh packages...
success Installed "create-next-app@13.2.4" with binaries:
      - create-next-app
✔ What is your project named? … [repo-name]
✔ Would you like to use TypeScript with this project? … No / Yes←
✔ Would you like to use ESLint with this project? … No / Yes←
✔ Would you like to use `src/` directory with this project? … No / Yes←
✔ Would you like to use experimental `app/` directory with this project? … No← / Yes
✔ What import alias would you like configured? … @/*

project nameや No/Yes の選択は適宜自由に入力してください。
この記事では上記の選択通りに進めるので特に理由がなければ同じ選択推奨です。

ルートページを編集

src/pages/index.tsxの中身を一旦全部消してシンプルな内容に編集します。
いらないファイル等も消してしまって大丈夫です。

src/pages/index.tsx
const Home = () => {
  return (
    <h1>掲示板!!!</h1>
  );
};
export default Home;

立ち上げて開いてみます。

$ yarn dev
  yarn run v1.22.19
  $ next dev
  ready - started server on 0.0.0.0:3000, url: http://localhost:3000


src/styles/globals.cssのダークモードがきいていて背景が黒くなっていますが気にせず。

これをGitHubにリポジトリを作ってpushしておきます。

$ git init
$ git add -A
$ git commit -m "Create next app"
$ git branch -M main
$ git remote add origin git @github.com[user-name]/[repo-name].git
$ git push -u origin main

2. NextをVercelでホスティングする

Vercelのアカウント登録をします。GitHubで登録/ログインしてください。
https://vercel.com/signup

  • Add New... > Projectをクリック
  • 今回作成したリポジトリのImportをクリック
  • Framework PresetNext.jsを選択、Deployをクリック
  • 少し待つとデプロイが完了します!

以降はmainブランチにgit pushするごとに自動でデプロイされるようになります。

3. GitHub GraphQL APIを叩いてみる

@octokit/graphql

GraphQLを呼び出すのに今回は@octokit/graphqlを利用して楽をします。
GitHub APIやGraphQLの設定などをいい感じに吸収してくれています。
https://github.com/octokit
https://github.com/octokit/graphql.js

$ yarn add @octokit/graphql

トークン取得/設定

またGitHub GraphQL APIを呼び出すのにPublicなデータを取るとしてもトークンが必要なので取得します。
以下の理由からPersonal Access Token (classic)の方で生成します。
https://docs.github.com/ja/graphql/guides/forming-calls-with-graphql#authenticating-with-graphql

注: GraphQL API の認証には personal access token (classic)、GitHub App、または OAuth App を作成する必要があります。 GraphQL API は fine-grained personal access token を使った認証をサポートしていません。

https://github.com/settings/tokens/new
権限はrepo / public_repoだけONにすれば大丈夫です。

生成できたら.env.localをルートディレクトリに置いて環境変数としてトークンを追加します。

.env.local
GIHUB_TOKEN=ghq_abcdefg123...

https://vercel.com/dashboard > [repo-name] > Settings > Environment Variables
から同じく GITHUB_TOKEN を追加しておいてください。

ここに追加されていればOKです。

GitHub GraphQL Explorer

いくつかissueを作っておきます。

あるリポジトリに存在するissue一覧を取得するqueryをGraphQL APIに投げてみます。
queryの組み立てはGitHub GraphQL APIが公開しているエクスプローラーを利用します。
なんかいい感じに入力補完してくれて実際にAPIを叩いてくれる優れものです。
https://docs.github.com/graphql/overview/explorer

では出来上がったqueryを用いてコードを書いていきます。

Next.js API Routes

Next.jsにはAPI Routesという機能があり、外部サービスを呼び出す時に便利です。
外部サービスを利用するとなると基本的にクレデンシャルを必要とすることが多いのでフロントから直接呼び出すわけにもいかず、サーバーサイドで動作しているNext.jsのAPIを利用すると安心です。

まずはAPIから。

src/pages/api/issues.ts
import { graphql } from "@octokit/graphql";
import { NextApiRequest, NextApiResponse } from "next";

// API Clientの初期設定これだけ
const graphqlClient = graphql.defaults({
  headers: {
    authorization: `Token ${process.env.GITHUB_TOKEN}`,
  },
});

// GraphQL query
// ステータスOPENのissueから`$issueCount`件を取る
// issueの`number`はURLで見えるIDぽいもの(IDは別である)
//   https://kthatoto/message-boards/issues/<number>
const getIssuesQuery = `
  query($owner: String!, $repository: String!, $issueCount: Int!) {
    repository(owner: $owner, name: $repository) {
      issues(first: $issueCount, states: OPEN) {
        nodes {
          number
          title
          body
        }
      }
    }
  }
`;

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const issuesResponse = await graphqlClient(getIssuesQuery, {
    // 引数を渡すことができる
    owner: "kthatoto",
    repository: "message-boards",
    issueCount: 50,
  });
  return res.status(200).json(issuesResponse);
}

APIを呼び出す部分

(この段階では省略しますが後でAPIを呼び出す部分はカスタムフックに切り出します)

src/pages/index.tsx
import { useEffect, useState } from "react";

const Home = () => {
  const [issues, setIssues] = useState([]);
  useEffect(() => {
    (async () => {
      const res = await fetch("/api/issues");
      const json = await res.json();
      console.log(json);
      setIssues(json.repository.issues.nodes);
    })();
  }, []);

  return (
    <>
      <h1>掲示板!!!</h1>
      <ul>
        {issues.map((issue) => (
          <li>Name:{issue.title}, ID: {issue.number}</li>
        ))}
      </ul>
    </>
  );
};

export default Home;

console.logの内容こんな感じ。

{
    "repository": {
        "issues": {
            "nodes": [
                {
                    "number": 1,
                    "title": "掲示板1",
                    "body": "適当な掲示板1"
                },
                {
                    "number": 2,
                    "title": "掲示板2",
                    "body": "適当な掲示板2"
                },
                {
                    "number": 3,
                    "title": "掲示板3",
                    "body": "適当な掲示板3"
                }
            ]
        }
    }
}

issue一覧取得できました!

4. 掲示板実装

では掲示板を作っていきます。

まずは必要なライブラリのインストールから。

// Mantine関連
$ yarn add @mantine/core @mantine/hooks @mantine/form @emotion/react

// GitHub GraphQL API (既に上の工程でインストール済みの場合はスキップでOK)
$ yarn add @octokit/graphql

各々要所を簡単に解説します。

API - issue一覧取得/作成

https://github.com/kthatoto/message-boards/blob/zenn/src/pages/api/issues.ts

このAPIエンドポイントにはGETリクエスト(issue一覧取得)に加えてPOSTリクエスト(issue作成)を追加しました。

https://github.com/kthatoto/message-boards/blob/zenn/src/pages/api/issues.ts#L31-L34

34行目でrepository.idを取得していますが、これはissueを作成する時の引数に利用します。
(下55,56行目)

https://github.com/kthatoto/message-boards/blob/zenn/src/pages/api/issues.ts#L54-L56

またissue一覧で各々のコメント数もとっています。
https://github.com/kthatoto/message-boards/blob/zenn/src/pages/api/issues.ts#L45-L47

API - issue詳細取得

https://github.com/kthatoto/message-boards/blob/zenn/src/pages/api/issues/[issueNumber].ts

issue詳細(タイトル,descriptionなど)とコメント一覧を取っています。

APIエンドポイント(ファイル名)が/api/issues/[issueNumber]になっていますが、Next.jsのDynamic API Routesという機能を利用しています。
https://nextjs.org/docs/api-routes/dynamic-api-routes

この部分でissueNumberを取っています。
https://github.com/kthatoto/message-boards/blob/zenn/src/pages/api/issues/[issueNumber].ts#L49-L50

またリポジトリにissueを追加する関係と同様、issueにコメントを投稿するのにもissue.numberとは別のissue.idが必要なので取っています。
https://github.com/kthatoto/message-boards/blob/zenn/src/pages/api/issues/[issueNumber].ts#L32-L33

API - コメント投稿

https://github.com/kthatoto/message-boards/blob/zenn/src/pages/api/issues/[issueNumber]/comments.ts

上述した通りissue.idを以ってコメント投稿します。
https://github.com/kthatoto/message-boards/blob/zenn/src/pages/api/issues/[issueNumber]/comments.ts#L14-L16

引数名がsubjectIdになっているのはおそらくPull Requestにコメント追加することもできるからです。
repository.idもそうでしたが、IDは接頭辞でリソースタイプを表しています。
例えばrepository.idならR_kgDOIiwHoAissue.idならI_kwDOIiwHoM5X4l0cという感じです。

pages/_app.tsx

https://github.com/kthatoto/message-boards/blob/zenn/src/pages/_app.tsx

Mantine Providerで囲うのと、AppShellで少し手を加えました。
Mantine Providerで囲うことは必須ですがAppShellはなくても大丈夫です。

ついでに言及しておくとpages/_document.tsxは初期生成のままです。

Page - / (掲示板一覧ページ)

https://github.com/kthatoto/message-boards/blob/zenn/src/pages/index.tsx

APIの呼び出しをカスタムフックに切り出したので状態管理はそっちでやっています。

フォームの入力制御,バリデーションはuseForm(@maintine/form)を利用しています。
https://github.com/kthatoto/message-boards/blob/zenn/src/pages/index.tsx#L41-L54
formタグで囲い、47行目で入力制御。51行目のようにbuttontype="submit"にすることでform.onSubmitが呼ばれます。

バリデーションはuseFormの引数に渡してメッセージを返すとform.onSubmitが呼ばれずコケてバリデーションメッセージが勝手に表示されるようになっています。Mantine便利!
https://github.com/kthatoto/message-boards/blob/zenn/src/pages/index.tsx#L27-L29

Page - /issues/[issueNumber] (掲示板詳細ページ/コメント一覧ページ)

https://github.com/kthatoto/message-boards/blob/zenn/src/pages/issues/[issueNumber].tsx

issueの詳細とコメント一覧を表示、コメントフォームがあります。
ここのページで特筆することはないので次。

hooks - useIssues

https://github.com/kthatoto/message-boards/blob/zenn/src/hooks/useIssues.ts

getIssuescreateIssuesを置いています。

https://github.com/kthatoto/message-boards/blob/zenn/src/hooks/useIssues.ts#L35-L42

getIssuesに関してこのアプリケーションでは外から呼び出すことがないためuseIssuesを呼ばれた時点で内部で呼ばれて良いので勝手に実行してレスポンスをstateにセットしています。
ここでrepository.idを取得、stateにセットしておくことでcreateIssueで利用しています。
useEffect内でasync awaitするやつで。

https://github.com/kthatoto/message-boards/blob/zenn/src/hooks/useIssues.ts#L17-L30

createIssueでもfetchを使っています。単にめんどくさかったインストールするライブラリを減らしたかったのでaxiosを入れずにfetchで。
mutationの返却値としてqueryで取得しているissueと同じ形を指定して、返ってきたものをそのまま配列に追加することで再取得せずに反映しています。(29,30行目)

hooks - useIssue

https://github.com/kthatoto/message-boards/blob/zenn/src/hooks/useIssue.ts

基本上のuseIssuesと同様の実装なのでここでも特筆することはありません。
repository/issuesに対してそれぞれがissue/commentsに対応する感じです。

以上でコードの解説は終了です!

あとがき

我ながらGitHubのissue機能を掲示板にするというのは面白いアイデアだと思っていて、
掲示板の代わりにissue.descriptionをブログ記事の内容として扱い外部ユーザーからはコメント投稿だけできるようにすればシンプルなブログサービスを作ることもできます。
実際少し前に作っていて、逆にこの記事用に掲示板サービスという体裁にしました。(Vue x Netlify)
issueにタグをつけることができるのでそれをqueryに数行追加し記事のタグ要素として表示することもしていました。そのあたり新しくAPIを叩く必要のないGraphQLの柔軟性のいいところですね。
今回はログイン機能を実装せずに全てのAPI呼び出しを前述した通り自分のアカウントとして行っているのでコメントを全て自分がすることになっていますがGitHubログイン機能を追加すればログインしてくれたユーザーが自身のアカウントとしてコメントする機能を実装することもできます。これも前のブログサービスでは実現していました。


長々とお読みいただきありがとうございました!
もう少しスッキリと書けると良かったのですが難しいものです。
逆にGraphQLのquery/mutationの解説を入れることができずでした。(流石に長くなりすぎる)
今後も少し変わった作ってみた/やってみたを記事として書いていこうと思っているので楽しみにしていてください。(少し変わってない記事も書くと思います)

さいごに

株式会社クロスビットでは、デスクレスワーカーのためのHR管理プラットフォームを開発しています。
一緒に開発を行ってくれる各ポジションのエンジニアを募集中です。

https://x-bit.co.jp/recruit/
https://herp.careers/v1/xbit
https://note.com/xbit_recruit

クロスビットテックブログ

Discussion