はじめに
本資料では、ハンズオンを想定し、Next.js と StepZen を利用し todo アプリを開発することで、GraphQL を用いた モダンなWeb アプリの構築方法を学びます。また、GraphQL を用いる際の技術選定の参考になればと思います。StepZen を用いない場合でも、 Apollo ClientやGraphQL Code Generator を用いた Next.js アプリの参考になるかと思います。
対象者
- Web サービスを迅速に立ち上げたい方
- GraphQL によるモダンなアプリ開発に関心がある方
- Web サービスの技術選定を検討中の方
前提条件
参加者は、Web アプリケーション開発に関する基本的な知識を有していることが前提となります。
本ハンズオンの内容
すること
- Next.js と StepZen での todo アプリ作成
- StepZen を利用しての GraphQL API サーバーの高速構築
- GraphQL Code Generator でのクライアントコードの自動生成
- Apollo Client を使用した GraphQL のクライアント実装
しないこと
- React/Next.js や StepZen の詳細説明
- 認証・認可やセキュリティ関連の実装
- エラーハンドリングの実装
- Vercel を利用した Web アプリのデプロイ
StepZenについては、以前書いた記事(GraphQL/StepZen ハンズオン資料 #1 - Qiita)もご参照ください。
作成するアプリケーション
このハンズオンを完了すると、以下の機能を持った todo アプリケーションが完成します。
- Todo 一覧表示: すべての todo アイテムを一覧できるページ。
- Todo 追加機能: 新しい todo アイテムを追加できるフォーム。
- Todo 編集機能: 既存の todo アイテムの完了・未完了を変更する機能。
- Todo 削除機能: 任意の todo アイテムを削除することができるボタン。
- データベースとの連携: ユーザーが入力した todo アイテムは PostgreSQL に保存され、アプリケーションの再起動後もデータは維持されます。
完成アプリケーションのイメージ
また、ソースコードは 下記 GitHub リンク からアクセスできます。参考にしてください。
optimisuke/todoapp-nextjs-stepzen: Next.js と StepZen を使った todo アプリ
アプリケーションの構成
使用する技術・サービス
- 開発言語・フレームワーク: Node.js, Next.js, React, TypeScript
- データベース: PostgreSQL
- ツール & ライブラリ: GraphQL Code Generator, Apollo Client, MUI
- マネージドサービス: StepZen (RDB や REST API を GraphQL API に変換), Neon (PostgreSQL のマネージドサービス)
手順概要
以下の手順でハンズオンを進めます。
- Node.js インストール
- Next.js/React を使った todo アプリ画面構築
- PostgreSQL/Neon を使った todo データベース構築
- StepZen を使った GraphQL API サーバー構築
- GraphQL Code Generator を使った クライアントコード自動生成
- Next.js/React からの GraphQL API 呼び出し
手順詳細
1. Node.js インストール
下記サイトからインストーラーをダウンロードしてインストールしてください。
他にも、VoltaやHomebrewを用いてインストールする方法もあります。ご自身の環境に合わせてインストールしてください。
2. Next.js/React を使った todo アプリ画面構築
次に、Next.js/React を用いてアプリ画面を構築します。ここでは、バックエンド API はまだ呼び出しません。
まず、作業フォルダにて、create-next-app
を用いて環境を構築します。
npx create-next-app@latest --typescript
対話的に質問が発生しますが、デフォルトの設定で問題ありません。
作業フォルダにmy-app
フォルダが作成されますので、フロントエンド(Next.js/React)の作業をする際は、my-app
フォルダにてコマンドを実行ください。
動作確認のため、下記コマンドを実行します。
cd my-app
npm run dev
ブラウザにてhttp://localhost:3000/にアクセスすると、以下のような Next.js の初期画面が表示されますので、確認ください。npm run dev
コマンドは必要に応じて、Ctrl-C
で停止してください。
次に、今回使用するパッケージをインストールします。
主に MUI 関係のものを選択しています。
npm install @mui/material @emotion/react @emotion/styled @mui/icons-material
次に、app フォルダを削除し、pages フォルダと components フォルダを作成します。
そして、以下の 4 ファイルを作成します。
pages/index.tsx
components/TodoList.tsx
components/TodoInput.tsx
components/TodoItem.tsx
4つのファイルの中身について、ここから説明していきます。
まず、トップページとして、下記ファイルでタイトルと TodoList コンポーネントを表示します。
MUI のコンポーネントを用いて見た目を調整しています。
import { Typography, Container } from "@mui/material";
import React from "react";
import TodoList from "../components/TodoList";
const IndexPage: React.FC = () => {
return (
<Container maxWidth="sm">
<Typography variant="h4" align="center" gutterBottom>
ToDo List
</Typography>
<TodoList />
</Container>
);
};
export default IndexPage;
次に、TodoList コンポーネントでは、TodoInput コンポーネントと、TodoItem コンポーネントのリストを表示します。
また、task をステートとして管理し、追加、チェックボックスのトグル処理、削除の関数を記載します。
追加の関数は TodoInput コンポーネントに渡し、トグルと削除の関数は、TodoItem コンポーネントに渡します。
import { List } from "@mui/material";
import React, { useState } from "react";
import TodoItem from "./TodoItem";
import TodoInput from "./TodoInput";
type Task = {
task: string;
completed: boolean;
};
const TodoList: React.FC = () => {
const [tasks, setTasks] = useState<Task[]>([]);
const addTask = (task: string) => {
setTasks([...tasks, { task, completed: false }]);
};
const toggleTask = (index: number) => {
const newTasks = [...tasks];
newTasks[index].completed = !newTasks[index].completed;
setTasks(newTasks);
};
const deleteTask = (index: number) => {
const newTasks = [...tasks];
newTasks.splice(index, 1);
setTasks(newTasks);
};
return (
<div>
<TodoInput addTask={addTask} />
<List>
{tasks.map((task, index) => (
<TodoItem
key={index}
task={task.task}
completed={task.completed}
toggleTask={() => toggleTask(index)}
deleteTask={() => deleteTask(index)}
/>
))}
</List>
</div>
);
};
export default TodoList;
TodoInput コンポーネントは、form を使って、追加処理を実施します。
また、TextField に入力した文字列をステートとして管理し表示できるようにします。
import { TextField, Button, Box } from "@mui/material";
import React, { useState } from "react";
type TodoInputProps = {
addTask: (task: string) => void;
};
const TodoInput: React.FC<TodoInputProps> = ({ addTask }) => {
const [task, setTask] = useState("");
const submitTask = (e: React.FormEvent) => {
e.preventDefault();
if (task === "") return;
addTask(task);
setTask("");
};
return (
<form onSubmit={submitTask}>
<Box display="flex" justifyContent="center" gap="10px" p={2}>
<TextField
label="New task"
variant="outlined"
value={task}
onChange={(e) => setTask(e.target.value)}
/>
<Button type="submit" variant="contained" color="primary">
Add
</Button>
</Box>
</form>
);
};
export default TodoInput;
TodoItem コンポーネントでは、チェックボックスと todo タスクのタイトル、削除アイコンを表示しています。
import {
ListItem,
ListItemText,
IconButton,
ListItemSecondaryAction,
} from "@mui/material";
import DeleteIcon from "@mui/icons-material/Delete";
import CheckCircleOutlineIcon from "@mui/icons-material/CheckCircleOutline";
import RadioButtonUncheckedIcon from "@mui/icons-material/RadioButtonUnchecked";
type TodoItemProps = {
task: string;
completed: boolean;
toggleTask: () => void;
deleteTask: () => void;
};
const TodoItem: React.FC<TodoItemProps> = ({
task,
completed,
toggleTask,
deleteTask,
}) => {
return (
<ListItem>
<IconButton edge="start" color="inherit" onClick={toggleTask}>
{completed ? <CheckCircleOutlineIcon /> : <RadioButtonUncheckedIcon />}
</IconButton>
<ListItemText
primary={task}
style={{ textDecoration: completed ? "line-through" : "none" }}
/>
<ListItemSecondaryAction>
<IconButton edge="end" color="inherit" onClick={deleteTask}>
<DeleteIcon />
</IconButton>
</ListItemSecondaryAction>
</ListItem>
);
};
export default TodoItem;
下記コマンドで動作確認します。
npm run dev
http://localhost:3000 に接続すると、以下のような画面が表示されると思います。
ぜひ、todoの追加や削除を試してみてください。ブラウザのメモリ上でタスクが保存されているため、リロードすると状態が消えてしまいますが、作成するtodo アプリの機能を確認することができます。次からは、データを永続化するために、データベースとGraphQL APIを作成していきます。
3. PostgreSQL/Neon を使った todo データベース構築
ここまでは、フロントエンドのみで Todo アプリを作成しました。次から、データベースを準備してデータを永続化できるようにセットアップしていきます。
Neon は PostgreSQL のマネージドサービスです。制限はありますが無料枠で非常に簡単に使えます。
下記手順に従って、データベースを作成していきます。
まず、トップページから画面のアカウントを作成します。
Github, Google Hasura のアカウントで Sign up できます。
Sign up後ログインすると以下のようなページが表示されます。
まず、プロジェクトを作成します。
SQL Editor
にSQLを書くことでデータベースを操作できます。
下記 SQL を用いてテーブルを作成します。先ほどのSQL Editor
の画面にSQLをコピペして、Run
を押すことでSQLを実行でいます。
今回、テーブルは基本的な情報のみとしました。また、id は UUID を使用することにしました。
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE TABLE todos (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
title VARCHAR(255) NOT NULL,
completed BOOLEAN DEFAULT false NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
);
後ほどテーブルの中身を確認するため、データも入れておきます。先ほどと同様、SQL Editor
を用いて以下のSQLを実行します。
INSERT INTO todos (title, completed)
VALUES ('新しいToDo', false);
また、update_at が自動更新されるようにトリガーを登録しておきます。
update_at の値をどこで生成するか、いくつかの選択肢がありますが、バックエンドサーバー(API サーバー)は StepZen で自動生成するためロジックを入れにくく、フロントエンド(クライアント)ではユーザー環境に依存する可能性があるので、データベースで更新することにしました。
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trigger_name_before_update
BEFORE UPDATE ON todos
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
Tables
のページで、テーブルが作成されていることとデータが入力されていることを確認できます。
これで、データベースの構築が完了しました。次は、StepZen を使ってデータベースを GraphQL API 化していきます。
4. StepZen を使った GraphQL API サーバー構築
StepZenはデータベース等を簡単にGraphQL APIサーバーとしてデプロイするクラウドサービスとなっています。ブラウザ上のダッシュボードとコマンドラインインターフェースを用いて操作します。最初にアカウントを作成とコマンドラインのインストールを行い、コマンドラインを使いながらGraphQL APIサーバーを構築していきます。
まず始めに、StepZen のウェブサイトにアクセスして、アカウントを作成しましょう。
次に、StepZen CLI をインストールします。
以下、Next.js/Reactのmy-app
フォルダのとなりに、api
フォルダを作成して作業していきます。
mkdir api
cd api
StepZen CLI を使用すると、ローカルマシンから StepZen サービスに直接アクセスできます。CLI をインストールするには、次の npm コマンドを実行します。(Node.jsがインストールされていることを前提としています。)-g
をつけることでNodeのグローバルインストールとしています。インストールコマンドはどの場所で実行しても問題ありません。
npm install -g stepzen
もし、permission denied
を含むエラーが出た場合、フォルダのパーミッションの問題と考えられるのでsudo npm install -g stepzen
を試してみてください。
インストール後、以下のコマンドを使って StepZen にログインします。ログインコマンドもどの場所で実行しても問題ありません。
stepzen login
下記プロンプトが出てくるので、それぞれStepZen Dashboardの鍵のアイコンの Account ページから取得したアカウント名と Admin Key を入力ください。
What is your account name?: ACCOUNT_NAME
What is your admin key?: ADMIN_KEY
これで StepZen のセッティングが完了しました。
次に、下記コマンドを実行し、GraphQL スキーマと StepZen ビルド用の構成ファイルを生成します。このコマンドはStepZen用のフォルダとして作ったapi
フォルダにて実行していきます。
stepzen import postgresql
以下のように対話的に必要な項目を入力していきます。
StepZenのユーザー情報やエンドポイントの名前、Neonで確認した接続情報を入力していきます。Neonに記載のある接続情報は、postgres://username:password@host/databasename
となっているので、質問に対してusername
, password
, host
, databasename
をコピーして、それぞれの情報を記載ください。
また、schemaについての質問はdefaultにするためにブランクとし、最後の質問はYesにするためにy
を入力しました。
$ stepzen import postgresql
? What would you like your endpoint to be called? api/todos
stepzen import postgresql - introspect a PostgreSQL database and extend your GraphQL schema with the types, queries and mutations for accessing it through a StepZen API.
? What is your host? xxx.example.com
? What is the username? optimisuke
? What is the password? [hidden]
? What is your database name? neondb
? What is your database schema (leave blank to use defaults)?
? Automatically link types based on foreign key relationships using @materializer
(https://stepzen.com/docs/features/linking-types) Yes
Starting... done
Successfully imported postgresql data source into your GraphQL schema
次に、postgresql/index.graphql
を修正していきます。
insert する時に id や update_at が不要になるように修正します。
title は必須とし completed はオプションとしています。
completed が入っていなかった場合を考えてsqlの関数であるCOALESCE
を使用しています。
以下に変更前と変更後の箇所を抜粋して記載します。
変更前(l. 54 周辺 type Mutation の中)
insertTodos(
id: ID!
completed: Boolean
created_at: DateTime
title: String!
updated_at: DateTime
): Todos
@dbquery(
type: "postgresql"
schema: "public"
table: "todos"
dml: INSERT
configuration: "postgresql_config"
)
変更後
insertTodos(title: String!, completed: Boolean): Todos
@dbquery(
type: "postgresql"
schema: "public"
query: """
INSERT INTO todos (title, completed) VALUES (
$1,
COALESCE($2, false)
)
RETURNING *
"""
configuration: "postgresql_config"
)
準備ができたら、api
フォルダにて下記コマンドでデプロイします。
stepzen start
上記コマンドは、スキーマの修正保存ごとにデプロイが実行されます。必要に応じて、Ctrl-C
で停止してください。
StepZen Dashboardの Explorer 画面でDocs
やBuilder
で指定したスキーマの API が作られていることが確認できます。
これで、GraphQL APIの構築が完了しました。次からは、クライアント側のNext.js/Reactのコードを修正することで、GraphQL APIの呼び出しを実装していきます。
5. GraphQL Code Generator を使った クライアントコード自動生成
必要なデータを取得するクエリを準備して、GraphQL Code Generator を使ってクライアントコードを生成します。
以下では、2章で作成したmy-app
フォルダに移動して、作業していきます。
cd my-app
まず、StepZen Dashboard で動作を確認しながらクエリを準備します。
以下のように、CRUD 操作を行うクエリを作成します。ファイルは、2章で作成したmy-app
フォルダのcomponents
に置くことにします。
query getTodos {
todosList {
id
title
completed
created_at
updated_at
}
}
mutation updateTodo($id: ID!, $completed: Boolean, $title: String) {
updateTodos(id: $id, completed: $completed, title: $title) {
completed
created_at
id
title
updated_at
}
}
mutation insertTodo($title: String!, $completed: Boolean) {
insertTodos(title: $title, completed: $completed) {
completed
created_at
id
title
updated_at
}
}
mutation deleteTodo($id: ID!) {
deleteTodos(id: $id) {
completed
created_at
id
title
updated_at
}
}
次に、GraphQL Code Generator に必要なものをインストールします。
npm install graphql @graphql-codegen/client-preset
npm i -D @graphql-codegen/cli
設定ファイルを準備します。クエリの.graphql
ファイルと StepZen 上のスキーマファイルから、./src/gql/
フォルダに型情報等のコードを生成します。
import { CodegenConfig } from "@graphql-codegen/cli";
import * as dotenv from "dotenv";
dotenv.config();
const apiKey = process.env.NEXT_PUBLIC_API_KEY || "invalid-api-key";
const uri = process.env.NEXT_PUBLIC_API_URI || "invalid-api-uri";
const config: CodegenConfig = {
schema: [
{
[uri]: {
headers: {
Authorization: `Apikey ${apiKey}`,
},
},
},
],
documents: ["./**/*.graphql"],
ignoreNoDocuments: true, // for better experience with the watcher
generates: {
"./src/gql/": {
preset: "client",
},
},
};
export default config;
.env
ファイルに下記のようにスキーマの URL と API Key を記載します。
NEXT_PUBLIC_API_URI=https://xxx.stepzen.net/api/todos/__graphql
NEXT_PUBLIC_API_KEY=xxx::stepzen.net+1000::yyy
API Key は StepZen の下記画面から取得できます。
package.json
のscripts
に自動生成コマンドのショートカットを追加します。
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"codegen": "graphql-codegen-esm --config codegen.ts"
},
下記コマンドで自動生成を実施します。
npm run codegen
my-app/src/gql/
にコードが生成されていることを確認します。
6. Next.js/React からの GraphQL API 呼び出し
Next.js/React のコードから自動生成したクライアントを使用して、StepZen で構築した GraphQL API を呼び出していきます。
まず、必要なパッケージをインストールします。
npm install @apollo/client
まず、pages/_app.tsx
を追加し Apollo Client を使用できるように記載します。
API Key と GraphQL API のスキーマの URI は、congen.ts
と同様、.env
ファイルから取得します。
import { ApolloProvider } from "@apollo/client";
import { AppProps } from "next/app";
import { ApolloClient, HttpLink, InMemoryCache } from "@apollo/client";
import { setContext } from "@apollo/client/link/context";
const apiKey = process.env.NEXT_PUBLIC_API_KEY || "invalid-api-key";
const uri = process.env.NEXT_PUBLIC_API_URI || "invalid-api-uri";
const httpLink = new HttpLink({
uri: uri,
});
const authLink = setContext((_, { headers }) => {
return {
headers: {
...headers,
authorization: `Apikey ${apiKey}`,
},
};
});
const apolloClient = new ApolloClient({
cache: new InMemoryCache(),
link: authLink.concat(httpLink),
});
function MyApp({ Component, pageProps }: AppProps) {
return (
<ApolloProvider client={apolloClient}>
<Component {...pageProps} />
</ApolloProvider>
);
}
export default MyApp;
また、TodoList.tsx を以下のように変更します。@apollo/client
のuseQuery
とuseMutation
を用いて GraphQL API にアクセスします。また、その引数として GraphQL Code Generator で生成したオブジェクトを指定しています。
さらに、追加・修正・削除時に、update 関数を呼び出し、クエリで取得した todo 配列のキャッシュを更新しています。少し複雑なコードに見えるかもしれませんが、Apollo の公式ドキュメント(Apollo GraphQL Docs)を参照しながら確認してみてください。
import { List } from "@mui/material";
import React from "react";
import TodoItem from "./TodoItem";
import TodoInput from "./TodoInput";
import { useMutation, useQuery } from "@apollo/client";
import {
DeleteTodoDocument,
GetTodosDocument,
InsertTodoDocument,
UpdateTodoDocument,
} from "@/src/gql/graphql";
const TodoList: React.FC = () => {
const { data, loading, error } = useQuery(GetTodosDocument);
const [insertTodo] = useMutation(InsertTodoDocument, {
update(cache, { data }) {
const existingData = cache.readQuery({ query: GetTodosDocument });
cache.writeQuery({
query: GetTodosDocument,
data: {
todosList: [...existingData?.todosList!, data?.insertTodos!],
},
});
},
});
const addTask = (task: string) => {
insertTodo({ variables: { title: task } });
};
const [updateTodo] = useMutation(UpdateTodoDocument, {
update(cache, { data }) {
const existingData = cache.readQuery({ query: GetTodosDocument });
const updatedTodos = existingData?.todosList?.map((todo) =>
todo?.id === data?.updateTodos?.id ? data?.updateTodos! : todo
);
cache.writeQuery({
query: GetTodosDocument,
data: { todosList: updatedTodos },
});
},
});
const toggleTask = (index: string, completed: boolean) => {
updateTodo({ variables: { id: index, completed: !completed } });
};
const [deleteTodo] = useMutation(DeleteTodoDocument, {
update(cache, { data }) {
const existingData = cache.readQuery({ query: GetTodosDocument });
const updatedTodos = existingData?.todosList?.filter(
(todo) => todo?.id !== data?.deleteTodos?.id
);
cache.writeQuery({
query: GetTodosDocument,
data: { todosList: updatedTodos },
});
},
});
const deleteTask = (index: string) => {
deleteTodo({ variables: { id: index } });
};
return (
<div>
<TodoInput addTask={addTask} />
{loading && <p>Loading...</p>}
{error && <p>Error: {error.message}</p>}
<List>
{data?.todosList?.map((task, index) => (
<TodoItem
key={index}
task={task?.title!}
completed={task?.completed!}
toggleTask={() => toggleTask(task?.id!, task?.completed!)}
deleteTask={() => deleteTask(task?.id!)}
/>
))}
</List>
</div>
);
};
export default TodoList;
以上で、Next.js/React のコードの修正は完了です。
下記コマンドで実行して動作確認します。
npm run dev
http://localhost:3000 に接続すると、以下のような画面が表示されると思います。
GraphQL API を呼び出さない最初のバージョンと動きは同じですが、Neon のTables
からデータを見ると、実際にデータが永続化されていることがわかります。
以上で、今回のハンズオンは完了です。
おわりに
このハンズオンを通じて、Next.js, GraphQL, StepZen などの最先端技術を用いたモダンな Web アプリの開発手法を学ぶことができたと思います。実際の手順に沿ってアプリを構築し、一連の流れを体験することで、今後の技術選定やプロジェクトのスタートに役立てることができるでしょう。ご質問やご指摘があればコメント欄によろしくお願いいたします。