😋

API Routeを使ってフロントからGraphQLサーバーにアクセスする【Next.js】

2021/11/11に公開

はじめに

以下のような方を対象としています.

  • GraphQL聞いたことある
  • Next.jsでGraphQL使ってみたい
  • Apolloサーバーを立てたい

今回作成したリポジトリです.
https://github.com/FujimuraKaito/next_graphql_swr

GraphQLとは何か

GraphQLについては既に分かりやすい記事がたくさん出ているのでリンクを共有することで説明を省略させていただきます.
https://qiita.com/shotashimura/items/3f9e04b93e79592030a4
https://qiita.com/yoshii0110/items/fbee34d5c235a2a064f9

準備

今回はNext.jsでGraphQLを学んでいくのでNext.jsでプロジェクトを立ち上げます.
以下を実行して,プロジェクト名などを決めます.

yarn create next-app --typescript
  1. srcディレクトリをプロジェクトルートに作成し,pagesディレクトリとstylesディレクトリをsrc/へ移動させます.
  2. テンプレートコードを削除します.(/src/pages/index.tsxをきれいにする)
    最終的には以下のようなコードにします.
pages/index.tsx
import type { NextPage } from 'next'
import Head from 'next/head'
import Link from 'next/link'

const Home: NextPage = () => {
  return (
    <>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main>
        <Link href={`/jobs`}>
          <a>jobs</a>
        </Link>
      </main>
    </>
  )
}

export default Home

GraphQL APIからデータを取得する

まずは一般に公開されているエンドポイントにアクセスしてデータを取得してきます.
使用したのはPublic Graph APIsで公開されているGraphQL Jobsです.

ここで必要なパッケージをインストールします.graphql-requestswrをインストールしてください.

yarn add graphql-request swr

続いてsrc/ディレクトリにjobsディレクトリを用意します.その中にindex.tsxを用意し,以下のようにします.

pages/jobs/index.tsx
import { request, gql } from 'graphql-request'
import useSWR from 'swr'

const API = 'https://api.graphql.jobs/'

const query = gql`
  query {
    jobs {
      id
      title
      applyUrl
    }
  }
`

type FetchData = {
  jobs: {
    id: string
    title: string
    applyUrl: string
  }[]
}

const getJobs = () => {
  const { data, error } = useSWR<FetchData>(
    query,
    // fecther
    (query) => request(API, query)
  )

  if (error) return <div>failed to load</div>
  if (!data) return <div>loading...</div>
  return data.jobs.map((job) => (
    <div key={job.id}>
      <li>{job.title}</li>
      <li>{job.applyUrl}</li>
      <br />
    </div>
  ))
}

const JobPage = () => (
  <>
    <h1>Job List</h1>
    {getJobs()}
  </>
)

export default JobPage

解説
今回取得するデータ型はここで確認できます.この画面でSCHEMAタブをクリックすると様々なデータのデータ型を知ることができます.改良するときはここを見ると良いと思います.

useSWRについては他の記事で分かりやすいものが多いので共有することで省略させていただきます.
https://zenn.dev/uttk/articles/b3bcbedbc1fd00

この画面が作成できたら早速yarn devで立ち上げて確認してみましょう!
localhost:3000にアクセスし,リンクを踏むとデータをフェッチしていることが確認できます.
またトップページに戻りもう一度アクセスすることでデータがキャッシュされていることも確認できます.

GithubのGraphQL APIからデータを取得する

まずsrc/pages/index.tsxを以下のように変更します.

src/pages/index.tsx
 import type { NextPage } from 'next'
 import Head from 'next/head'
 import Link from 'next/link'

 const Home: NextPage = () => {
  return (
    <>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main>
        <Link href={`/jobs`}>
          <a>jobs</a>
        </Link>
+        <br />
+        <Link href={`/issues`}>
+          <a>issues</a>
+        </Link>
      </main>
    </>
  )
 }

 export default Home

次にGithubのアクセストークンを取得します.
以下の記事を参考にしてください.
トークンのスコープは全て選択して問題ないと思います.
https://docs.github.com/ja/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token

取得したアクセストークンはコピーしてプロジェクトルートの.env.localに追加します.

/.env.local
NEXT_PUBLIC_GITHUB_PERSONAL_ACCESSTOKEN=XXXXXXXX

そして/pagesディレクトリに新たにpage/issuesディレクトリを作成しその中にindex.tsxを作成します.

pages/issues/index.tsx
import { GraphQLClient, gql } from 'graphql-request'
import useSWR from 'swr'

const API = 'https://api.github.com/graphql' // endpoint
const repositoryOwner = 'vercel' // the repository owner
const repositoryName = 'next.js' // the repository name
const issuesFirst = 100 // the number of issues

const getRepositoryQuery = gql`
  query GetRepository(
    $repositoryOwner: String!
    $repositoryName: String!
    $issuesFirst: Int
  ) {
    repository(owner: $repositoryOwner, name: $repositoryName) {
      name
      issues(first: $issuesFirst) {
        edges {
          node {
            id
            title
          }
        }
      }
    }
  }
`

type FetchData = {
  repository: {
    name: string
    issues: {
      edges: {
        node: {
          id: string
          title: string
        }
      }[]
    }
  }
}

const getIssues = () => {
  // use GraphQLClient to set Header
  const client = new GraphQLClient(API, {
    headers: {
      Authorization:
        'bearer ' + process.env.NEXT_PUBLIC_GITHUB_PERSONAL_ACCESSTOKEN,
    },
  })

  const { data, error } = useSWR<FetchData>(
    [getRepositoryQuery, repositoryOwner, repositoryName, issuesFirst],
    // fetcher
    (query, owner, name, first) =>
      client.request(query, {
        // variables
        repositoryOwner: owner,
        repositoryName: name,
        issuesFirst: first,
      })
  )

  if (error) return <div>failed to load</div>
  if (!data) return <div>loading...</div>
  return data.repository.issues.edges.map((issue) => (
    <li key={issue.node.id}>{issue.node.title}</li>
  ))
}

const IssuesPage = () => (
  <>
    <h1>
      {repositoryOwner}/{repositoryName} Issue List
    </h1>
    {getIssues()}
  </>
)

export default IssuesPage

解説
今回はvercelのnext.jsリポジトリのissueを取得してきました.
まず前回と違う点はHeaderが付け加えられていることです.HeaderはGraphQLClientの第2引数で指定できます.

そしてuseSWRの第2引数で以下のようにしています.

pages/issues/index.tsx
(query, owner, name, first) =>
  client.request(query, {
  // variables
  repositoryOwner: owner,
  repositoryName: name,
  issuesFirst: first,
})

このvariablesの部分でQueryで指定した変数に値を渡すことができます.

pages/issues/index.tsx
const getRepositoryQuery = gql`
  query GetRepository(
    $repositoryOwner: String!
    $repositoryName: String!
    $issuesFirst: Int
  ) {
    repository(owner: $repositoryOwner, name: $repositoryName) {
      name
      issues(first: $issuesFirst) {
        edges {
          node {
            id
            title
          }
        }
      }
    }
  }
`

前章と同じようにyarn devでちゃんと動いているかどうかを確認しましょう!
ページにアクセスするとissueが表示されているはずです.
リポジトリ名を変えれば他のリポジトリについても同様に行えます.

API RouteにApollo Serverをたて,データを取得する

次にNext.jsが提供するAPI RouteにApollo Serverを立て,フロント側からアクセスし,データを取得してみましょう!

今回はapollo-server-microを使用するのでパッケージをインストールします.

yarn add apollo-server-micro

まずスキーマを定義します.
src/apolloディレクトリを作成してください.そしてその中にtype-defs.tsを作成します.

type-defs.ts
import { gql, Config } from 'apollo-server-micro'

export const typeDefs: Config['typeDefs'] = gql`
  type Article {
    id: Int
    title: String
    content: String
  }

  type Query {
    getArticle(id: Int): Article
    getArticles: [Article]
  }
`

今回はryo_kawamataさんのコードをほぼそのまま利用させていただいています.

続いてリゾルバを作成します.
src/apolloディレクトリにresolver.tsを定義します.

resolver.ts
import { Config } from 'apollo-server-micro'

const SAMPLE_DB = {
  articles: [
    { id: 1, title: 'foo', content: 'fooooo' },
    { id: 2, title: 'bar', content: 'baaaar' },
    { id: 3, title: 'baz', content: 'baaaaz' },
  ],
}

const getArticleResolver = (_: any, { id }: { id: number }) =>
  SAMPLE_DB.articles?.filter((article) => article.id === id)[0] ?? []

const getArticles = () => SAMPLE_DB.articles

export const resolvers: Config['resolvers'] = {
  Query: {
    getArticle: getArticleResolver,
    getArticles: getArticles,
  },
}

ここで実際にDBから取得する処理を書きます.

次にAPI Routeでサーバーを用意します.
src/pages/apiディレクトリにgraphql.tsを作成します.

graphql.ts
import { NextApiRequest, NextApiResponse } from 'next'

import { ApolloServer } from 'apollo-server-micro'
import Cors from 'micro-cors'

import { typeDefs } from '../../apollo/type-defs'
import { resolvers } from '../../apollo/resolver'

export const config = {
  api: {
    bodyParser: false,
  },
}

const apolloServer = new ApolloServer({ typeDefs, resolvers })
const startServer = apolloServer.start()

const handler = async (req: NextApiRequest, res: NextApiResponse) => {
  res.setHeader('Access-Control-Allow-Credentials', 'true')
  res.setHeader('Access-Control-Allow-Origin', 'https://example.com')
  res.setHeader(
    'Access-Control-Allow-Headers',
    'Origin, X-Requested-With, Content-Type, Accept'
  )
  if (req.method === 'OPTIONS') {
    res.end()
    return false
  }

  await startServer
  await apolloServer.createHandler({
    path: '/api/graphql',
  })(req, res)
}

export default handler

解説
next.jsのsampleを参考にしました.
またAccess-Control-Allow-Originも指定できます.さらにmicro-corsを使うことでCORSの設定もできるようです.
これでフロントエンドからアクセスできるようになりました!🎉

次にフロントエンドの実装をしていきます.
まずsrc/pages/index.tsxを以下のように変更します.

pages/index.tsx
 import type { NextPage } from 'next'
 import Head from 'next/head'
 import Link from 'next/link'

 const Home: NextPage = () => {
  return (
    <>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main>
        <Link href={`/jobs`}>
          <a>jobs</a>
        </Link>
        <br />
        <Link href={`/issues`}>
          <a>issues</a>
        </Link>
+        <br />
+        <Link href={`/articles`}>
+          <a>articles</a>
+        </Link>
      </main>
    </>
  )
 }

export default Home

次に/pagesディレクトリに新たにpage/articlesディレクトリを作成しその中にindex.tsxを作成します.実際にデータを取得する処理を追加します.
ここは前章までと同じでuseSWRを用いて実装しています.

pages/articles/index.tsx
import { GraphQLClient, gql } from 'graphql-request'
import useSWR from 'swr'

const API = '/api/graphql'

const getArticlesQuery = gql`
  query {
    getArticles {
      id
      title
      content
    }
  }
`

type FetchData = {
  getArticles: [
    {
      id: string
      title: string
      content: string
    }
  ]
}

const getArticles = () => {
  const client = new GraphQLClient(API)

  const { data, error } = useSWR<FetchData>(
    [getArticlesQuery],
    // fetcher
    (query) => client.request(query)
  )

  if (error) return <div>failed to load</div>
  if (!data) return <div>loading...</div>

  return data.getArticles.map((article) => (
    <li key={article.id}>{article.title}</li>
  ))
}

const ArticlePage = () => (
  <>
    <h1>Articles List</h1>
    {getArticles()}
  </>
)

export default ArticlePage

まずはarticleを全て取得するクエリを実行します.
ここまでできればyarn devで立ち上げ,localhost:3000/articlesに遷移するとデータがフェッチされます.

もっと使いやすく

今回はarticleを全て取得するクエリを実行しましたが,IDを指定してarticleを取得するリゾルバも用意しているので前章を参考にしてIDからarticleを取得することもできます.

pages/index.tsx
--省略--
+ <br />
+ <Link href={`/articles/1`}>
+   <a>articles/1</a>
+ </Link>

今までの部分と同様にarticles/1へのリンクを追加します.
(フォームか何かを設置して自分で変更できるようにするようが実用的かと思いますが)

そして次にpages/articles/[id].tsxを作成し,以下のようにします.

[id].tsx
import { useRouter } from 'next/router'

import { GraphQLClient, gql } from 'graphql-request'
import useSWR from 'swr'

const API = '/api/graphql'

const getArticleById = gql`
  query($getArticleId: Int!) {
    getArticle(id: $getArticleId) {
      id
      title
      content
    }
  }
`

type FetchData = {
  getArticle: {
    id: string
    title: string
    content: string
  }
}

const getArticles = () => {
  const router = useRouter()

  const { id } = router.query
  const client = new GraphQLClient(API)

  const { data, error } = useSWR<FetchData>(
    [getArticleById],
    // fetcher
    (query) =>
      client.request(query, {
        getArticleId: Number(id),
      })
  )
  console.log(data)

  if (error) return <div>failed to load</div>
  if (!data) return <div>loading...</div>

  return <li key={data.getArticle.id}>{data.getArticle.title}</li>
}

const SingleArticlePage = () => (
  <>
    <h1>Articles List</h1>
    {getArticles()}
  </>
)

export default SingleArticlePage

解説
Githubのissueを取得した時と同じようにQueryに変数を渡します.
client.requestの第2引数で指定します.

まとめ

実際に手を動かしながら作ってみると気づけることがたくさんありました.まだまだGraphQLでできることはたくさんありますが,同じような入門者の方の助けとなれば嬉しいです.

参考にさせていただいた記事など

https://zenn.dev/thim/articles/d09cc8500d47d3216907
https://zenn.dev/ryo_kawamata/articles/how-to-create-apollo-server-on-nextjs-api-routes
https://github.com/vercel/next.js/blob/canary/examples/api-routes-graphql/pages/api/graphql.js

Discussion