Hasura&Nextjsで快適な開発環境を構築〜ローカル環境構築編〜

こんにちは、株式会社エイチームコマーステックのkazzです。

私たちは、HasuraとNextjsという技術を使い、Obremoというペットフード販売プロダクトを開発しています。
今回は、そのHasuraとNextjsの組み合わせで実際に手を動かして環境構築をしてみましたので、ざっくりとした手順を備忘録としてまとめたいと思います。

## 構成

バックエンドはDockerイメージのHasura、データベースはPostgreSQL、フロントはNextjsとApolloClientを採用しています。HasuraとNextjsの環境構築と、ApolloClientを利用しクライアント連携を行い、データベース上のデータをブラウザに表示するところまで行います。

今回は、プロダクション環境をHasuraCloud上で動作させ、ローカル環境をDockerイメージで動作させるような想定で構成させます。(この編ではローカル環境のみ扱っています)

## Hasura

Hasuraはバックエンドで動作するGraphQLエンジンです。面倒な設定不要で比較的サクッと始められます。付属のGUI環境でデータベースやHasuraの便利機能をWebクライアント上から操作することができ、Hasura単体で開発も運用もめっちゃ楽になります。
PaaSとして動作するHasuraCloudが便利ですが、公式で配布されているDockerイメージを使うとオンプレでも起動させることができます。

### ローカル環境インストール・起動

公式から、PostgreSQLとHasuraがひとまとまりになっているdocker-coposeファイルが提供しているので活用します。

https://hasura.io/docs/latest/graphql/cloud/getting-started/index.html

```sh
mkdir hasura-and-nextjs
cd hasura-and-nextjs
curl https://raw.githubusercontent.com/hasura/graphql-engine/stable/install-manifests/docker-compose/docker-compose.yaml -o docker-compose.yml
docker-compose up -d
```

### テーブル・データ作成

[http://localhost:8080/console](http://localhost:8080/console)

コンソールへアクセスし、DATAタブからデータベースのあれこれを操作できます。

![スクリーンショット](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305522/24b6f539-d04b-e4e6-3b6c-0f6f2d7b6cf1.png)

まずは、接続先のDBを設定します。今回は`docker-compose.yml`に設定されている環境変数からURLを取得するように設定しています。

![スクリーンショット](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305522/9b229004-94c2-80e6-4dce-8775b8f89e14.png)

接続できたら、テーブルを作成します。

![localhost_8080_console_data_postgres_schema_public_table_add.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305522/c391a981-e700-2a58-0736-d5981555a6ce.png)

レコードも追加します。

![スクリーンショット](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305522/ccc7505c-9b66-d014-d9ee-292a63822d75.png)

## Nextjs

Hasuraバックエンドと連携するWebクライアントフレームワークです。Reactベースで開発でき、SPA、SR、SSG、ISRなど、多彩な利用シーンに対応できます。

今回は、Nextjsに搭載されていない機能をApolloClientというライブラリを使って補います。Apolloは、主にNextjsとGraphQLの繋ぎ込みに利用します。

### インストール

https://nextjs.org/docs/api-reference/create-next-app

```
npx create-next-app@latest --ts
> Ok to proceed? (y) 
> ✔ What is your project named? … front
cd front
npm i
npm run dev
```

追加で、これから必要なライブラリもインストールします。

```
npm install @apollo/client graphql cross-fetch @graphql-codegen/cli
```

[http://localhost:3000](http://localhost:3000)

`front/pages/index.tsx`をよしなに修正して起動してみると

![スクリーンショット 2021-12-13 11.29.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305522/e75df2cb-f03a-5095-7d8c-156bec2c2708.png)

### Hasuraとクライアントの連携

#### Codegenでスキーマの同期設定

Hasuraで定義したスキーマをNextjsで使用できる型定義などを出力してくれる`graphql-codegen`というツールがあるので、そちらも導入します。

```sh
npx graphql-codegen init
```

initをすると、対話形式で設定できます。

```
 ◯ Backend - API or server
 ◯ Application built with Angular
❯◉ Application built with React
 ◯ Application built with Stencil
 ◯ Application built with other framework or vanilla JS
```

```
? What type of application are you building? Application built with Re
act
? Where is your schema?: (path or url) http://localhost:8080/v1/graphq
l
? Where are your operations and fragments?: queries/**/*.ts
? Pick plugins: TypeScript (required by other typescript plugins), Typ
eScript Operations (operations and fragments), TypeScript React Apollo
 (typed components and HOCs)
? Where to write the output: types/graphqlSchema.tsx
? Do you want to generate an introspection file? No
? How to name the config file? codegen.yml
? What script in package.json should run the codegen? graphql-codegen
```

こちらで設定した通り、ディレクトリを作成します。

```sh
mkdir queries
mkdir types
```

`queries`にクライアントから実行したいGraphQLコードを定義します。

```typescript:queries/posts.ts
import { gql } from "@apollo/client";

export const GET_POSTS = gql`
  query GetPost {
    posts(limit: 10) {
      content
      title
      id
    }
  }
`;
```
codegenのinit時にcodegenのエイリアスがpackage.jsonのscriptに定義されており、そちらを手動で実行します。実行すると、`types/graphqlSchema.ts`に型定義などが出力されます。

```sh
npm run graphql-codegen
> graphql-codegen
> graphql-codegen --config codegen.yml

(node:33488) ExperimentalWarning: stream/web is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
(node:33488) ExperimentalWarning: buffer.Blob is an experimental feature. This feature could change at any time
  ✔ Parse configuration
  ✔ Generate outputs
```

出力されたコードには、型定義やAPICall時に実行可能な関数などがまとまっています。

```typescript:types/graphqlSchema.ts
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
export type Maybe<T> = T | null;
export type InputMaybe<T> = Maybe<T>;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
const defaultOptions =  {}
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
  ID: string;
  String: string;
  Boolean: boolean;
  Int: number;
  Float: number;
};

...下に続く
```

#### ApolloClientで実際にfetchして画面に表示

以下のnextjsの公式exampleを参考に、実装を進めます。

[https://github.com/vercel/next.js/blob/canary/examples/with-apollo/lib/apolloClient.js](https://github.com/vercel/next.js/blob/canary/examples/with-apollo/lib/apolloClient.js)

ApolloClientでの連携に必要なインスタンスを生成するためのファイルを作成していきます。

```
mkdir libs
touch apolloClient.ts
```

```typescript:front/libs/apolloClient.ts
import {
  ApolloClient,
  HttpLink,
  InMemoryCache,
  NormalizedCacheObject,
} from "@apollo/client";
import "cross-fetch/polyfill";

let apolloClient: ApolloClient<NormalizedCacheObject> | undefined;
const createApolloClient = () => {
  return new ApolloClient({
    ssrMode: typeof window === "undefined",
    link: new HttpLink({
      // .env.development.localに`NEXT_PUBLIC_HASURA_URL='http://localhost:8080/v1/graphql'` と定義しておきます
      uri: process.env.NEXT_PUBLIC_HASURA_URL,
    }),
    cache: new InMemoryCache(),
  });
};
export const initializeApollo = (initialState = null) => {
  const _apolloClient = apolloClient ?? createApolloClient();
  // For SSG and SSR always create a new Apollo Client
  if (typeof window === "undefined") return _apolloClient;
  // Create the Apollo Client once in the client
  if (!apolloClient) apolloClient = _apolloClient;

  return _apolloClient;
};

```

こちらを`_app.tsx`で実行して、配下のコンポーネントがApolloClientを利用できるようにします。

```react
import "../styles/globals.css";
import type { AppProps } from "next/app";
import { ApolloProvider } from "@apollo/client";
import { initializeApollo } from "../libs/apolloClient";

function MyApp({ Component, pageProps }: AppProps) {
  const client = initializeApollo();
  return (
    <ApolloProvider client={client}>
      <Component {...pageProps} />
    </ApolloProvider>
  );
}

export default MyApp;
```

実際に`index.tsx`でfetchしてみましょう。

```react
import { useQuery } from "@apollo/client";
import type { NextPage } from "next";
import { GET_POSTS } from "../queries/posts";
import { GetPostQuery } from "../types/graphqlSchema";

const Home: NextPage = () => {
  const { data, error } = useQuery<MyQueryQuery>(GET_POSTS);
  return (
    <div>
      {data?.posts.map((post) => (
        <div key={`post-${post.id}`}>
          {post.id} {post.title}
          <br />
          {post.content}
        </div>
      ))}
    </div>
  );
};

export default Home;
```

ページを開くとデータが表示されました！

![スクリーンショット](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305522/ac384b13-d62e-f051-fe21-42a89aadff7c.png)

## プロダクション環境構築編

https://qiita.com/kazzzzzz/items/966019524c5203858a2d

## 〆

次回アドベントカレンダーの投稿は、「難しく考えずにベイジアンABテストを試してみた」です。
最後まで読んでいただきありがとうございました！