GitHub GraphQL APIで掲示板作ってみた
はじめに
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分クッキングのように)
使用技術/サービス
- Next.js x TypeScript
- Mantine (UIライブラリ)
- GitHub (BaaSとして)
- Vercel (ホスティングサービス)
実装する機能
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
の中身を一旦全部消してシンプルな内容に編集します。
いらないファイル等も消してしまって大丈夫です。
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 Preset
でNext.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
をルートディレクトリに置いて環境変数としてトークンを追加します。
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から。
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を呼び出す部分はカスタムフックに切り出します)
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一覧取得/作成
このAPIエンドポイントにはGETリクエスト(issue一覧取得)に加えてPOSTリクエスト(issue作成)を追加しました。
34行目でrepository.id
を取得していますが、これはissueを作成する時の引数に利用します。
(下55,56行目)
またissue一覧で各々のコメント数もとっています。
API - issue詳細取得
issue詳細(タイトル,descriptionなど)とコメント一覧を取っています。
APIエンドポイント(ファイル名)が/api/issues/[issueNumber]
になっていますが、Next.jsのDynamic API Routesという機能を利用しています。
https://nextjs.org/docs/api-routes/dynamic-api-routes
この部分でissueNumber
を取っています。
またリポジトリにissueを追加する関係と同様、issueにコメントを投稿するのにもissue.number
とは別のissue.id
が必要なので取っています。
API - コメント投稿
上述した通りissue.id
を以ってコメント投稿します。
引数名がsubjectId
になっているのはおそらくPull Requestにコメント追加することもできるからです。
repository.id
もそうでしたが、ID
は接頭辞でリソースタイプを表しています。
例えばrepository.id
ならR_kgDOIiwHoA
、issue.id
ならI_kwDOIiwHoM5X4l0c
という感じです。
pages/_app.tsx
Mantine Providerで囲うのと、AppShellで少し手を加えました。
Mantine Providerで囲うことは必須ですがAppShellはなくても大丈夫です。
ついでに言及しておくとpages/_document.tsx
は初期生成のままです。
Page - / (掲示板一覧ページ)
APIの呼び出しをカスタムフックに切り出したので状態管理はそっちでやっています。
フォームの入力制御,バリデーションはuseForm
(@maintine/form
)を利用しています。
formタグで囲い、47行目で入力制御。51行目のようにbutton
をtype="submit"
にすることでform.onSubmit
が呼ばれます。
バリデーションはuseForm
の引数に渡してメッセージを返すとform.onSubmit
が呼ばれずコケてバリデーションメッセージが勝手に表示されるようになっています。Mantine便利!
Page - /issues/[issueNumber] (掲示板詳細ページ/コメント一覧ページ)
issueの詳細とコメント一覧を表示、コメントフォームがあります。
ここのページで特筆することはないので次。
hooks - useIssues
getIssues
とcreateIssues
を置いています。
getIssues
に関してこのアプリケーションでは外から呼び出すことがないためuseIssues
を呼ばれた時点で内部で呼ばれて良いので勝手に実行してレスポンスをstateにセットしています。
ここでrepository.id
を取得、stateにセットしておくことでcreateIssue
で利用しています。
useEffect内でasync awaitするやつで。
createIssue
でもfetch
を使っています。単にめんどくさかったインストールするライブラリを減らしたかったのでaxios
を入れずにfetch
で。
mutationの返却値としてqueryで取得しているissueと同じ形を指定して、返ってきたものをそのまま配列に追加することで再取得せずに反映しています。(29,30行目)
hooks - useIssue
基本上のuseIssues
と同様の実装なのでここでも特筆することはありません。
repository
/issues
に対してそれぞれがissue
/comments
に対応する感じです。
以上でコードの解説は終了です!
あとがき
我ながらGitHubのissue機能を掲示板にするというのは面白いアイデアだと思っていて、
掲示板の代わりにissue.description
をブログ記事の内容として扱い外部ユーザーからはコメント投稿だけできるようにすればシンプルなブログサービスを作ることもできます。
実際少し前に作っていて、逆にこの記事用に掲示板サービスという体裁にしました。(Vue x Netlify)
issueにタグをつけることができるのでそれをqueryに数行追加し記事のタグ要素として表示することもしていました。そのあたり新しくAPIを叩く必要のないGraphQLの柔軟性のいいところですね。
今回はログイン機能を実装せずに全てのAPI呼び出しを前述した通り自分のアカウントとして行っているのでコメントを全て自分がすることになっていますがGitHubログイン機能を追加すればログインしてくれたユーザーが自身のアカウントとしてコメントする機能を実装することもできます。これも前のブログサービスでは実現していました。
長々とお読みいただきありがとうございました!
もう少しスッキリと書けると良かったのですが難しいものです。
逆にGraphQLのquery/mutationの解説を入れることができずでした。(流石に長くなりすぎる)
今後も少し変わった作ってみた/やってみたを記事として書いていこうと思っているので楽しみにしていてください。(少し変わってない記事も書くと思います)
さいごに
株式会社クロスビットでは、デスクレスワーカーのためのHR管理プラットフォームを開発しています。
一緒に開発を行ってくれる各ポジションのエンジニアを募集中です。
Discussion