🎉

Next.js, Prisma, Apollo GraphQL, Nexusで作るシンプルTODOアプリ

2021/12/06に公開

この記事はClassi developers Advent Calendar 2021の6日目の記事です。

この記事では、Next.jsをベースに、Prisma・Apollo GraphQL・GraphQL Nexusを組み合わせて簡易的なTODOアプリを実装してみたのでその手順をまとめます。

これらの組み合わせにより、バックエンドもフロントエンドもすべてTypeScriptで記述することができます。

全体構成

主に使用するフレームワーク・ライブラリ

  • Next.js
    • Reactフレームワーク
  • Prisma
    • Node.jsとTypeScriptのためのORM
    • クエリの結果が型付けされるため、開発体験を向上させることができる特徴がある
    • 本書ではPostgreSQLと接続してデータの取得・操作を行う
  • Apollo GraphQL
    • GraphQLサーバ(Apollo Server)およびGraphQLクライアント(Apollo Client)を提供するライブラリ
    • Apollo GraphQLはさまざまなサービスと接続し、あらゆるデータを集約することができる
    • 本書ではApollo ServerにPrismaを接続してデータの取得・操作を行い、Apollo ClientがNext.jsにデータを提供するようにする
  • GraphQL Nexus
    • Nexusを使うと、コードからGraphQLのスキーマ定義とTypeScriptの型定義ファイルを自動生成してくれる
    • これにより、スキーマと型定義が同期されるため、スキーマの変更が容易になる

システム構成

次のような構成でデータの取得や操作を行うようにします

img

作るもの

以下のようなシンプルなCRUD機能だけを持つTODOアプリを作ります

  • タスク一覧を表示できる(Read)
  • タイトルを入力してタスクを追加できる(Create)
  • タスクを削除できる(Delete)
  • チェックボックスをクリックして完了/未完了を変更できる(Update)

gif

DBスキーマ

簡易化のため以下のような属性を持つTaskエンティティのみを用意することにします

tasks
- id
- title
- done
- createdAt
- updatedAt

リポジトリ

最終的なコードはこのリポジトリで公開しています
https://github.com/youichiro/next-graphql-simple-todo-app

Next.jsプロジェクトの作成

次のコマンドでNext.jsのプロジェクトを作成します

npx create-next-app next-graphql-simple-todo-app --typescript

TypeScriptを使用するため--typescriptオプションを付けています

サーバを起動して http://localhost:3000 を開くとウェルカム画面が表示されます

cd next-graphql-simple-todo-app
npm run dev

img1

トップ画面のコードをクリーンにしておきます

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

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

      <main>
          <h1>Hello World</h1>
      </main>
    </div>
  )
}

export default Home

img2

Prismaでマイグレーションを実行する

Prismaのセットアップ

Prismaをインストールします

npm install prisma --save-dev

Prismaのセットアップをするため次のコマンドを実行します

npx prisma init
✔ Your Prisma schema was created at prisma/schema.prisma
  You can now open it in your favorite editor.

warn You already have a .gitignore. Don't forget to exclude .env to not commit any secret.

Next steps:
1. Set the DATABASE_URL in the .env file to point to your existing database. If your database has no tables yet, read https://pris.ly/d/getting-started
2. Set the provider of the datasource block in schema.prisma to match your database: postgresql, mysql, sqlite, sqlserver or mongodb (Preview).
3. Run prisma db pull to turn your database schema into a Prisma schema.
4. Run prisma generate to generate the Prisma Client. You can then start querying your database.

More information in our documentation:
https://pris.ly/d/getting-started

PostgreSQLのデータベースを作成する

今回はローカルのPostgreSQLをデータベースに使用します
PostgreSQLのセットアップが整っている前提で進めます

次のコマンドで新しくデータベースを作成します

createdb next_graphql_simple_todo_app

prisma initの時に作られた.envファイルを開き、環境変数DATABASE_URLの値を変更します

.env
DATABASE_URL="postgresql://<USER>:<PASSWORD>@localhost:5432/next_graphql_simple_todo_app?schema=public"

<USER>と<PASSWORD>には自分の情報を入れます

DBスキーマを定義する

prisma initの時に作られたprisma/schema.prismaにDBスキーマを定義します
今回はTaskエンティティのみ定義します

prisma/schema.prisma
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model Task {
  id          Int      @id @default(autoincrement())
  title       String   @db.VarChar(100)
  done        Boolean  @default(false)
  createdAt   DateTime @default(now()) @map(name: "created_at")
  updatedAt   DateTime @updatedAt @map(name: "updated_at")

  @@map(name: "tasks")
}

Prismaのスキーマ定義の記法は次のページをご覧ください
https://www.prisma.io/docs/concepts/components/prisma-schema/data-model

ちなみに次のコマンドで、このスキーマファイル(schema.prisma)のフォーマットを行うことができます

npx prisma format

マイグレーション

次のコマンドを実行して、マイグレーションを行います

npx prisma db migrate dev --name init

prisma/migrations/${timestamp}_init/migration.sqlにマイグレーションで発行されたSQLが保存されます

prisma/migration/20211124135755_init/migration.sql
-- CreateTable
CREATE TABLE "tasks" (
    "id" SERIAL NOT NULL,
    "title" VARCHAR(100) NOT NULL,
    "done" BOOLEAN NOT NULL DEFAULT false,
    "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
    "updated_at" TIMESTAMP(3) NOT NULL,

    CONSTRAINT "tasks_pkey" PRIMARY KEY ("id")
);

スキーマファイルの定義を追加や変更、削除した場合、再度マイグレーションコマンドを実行することで差分がprisma/migrationsに保存されていきます

マイグレーションに関するコマンドの詳細は次のページをご覧ください
https://www.prisma.io/docs/reference/api-reference/command-reference#prisma-migrate

seedデータを投入する

まず、PrismaClientのインスタンスを作成するために、lib/prisma.tsを作成します

lib/prisma.ts
import { PrismaClient } from '@prisma/client'

declare global {
  var prisma: PrismaClient | undefined
}

export const prisma =
  global.prisma ||
  new PrismaClient({
    log: ['query'],
  })

if (process.env.NODE_ENV !== 'production') global.prisma = prisma

このコードに関してはPrisma公式がベストプラクティスを提示しています
https://www.prisma.io/docs/support/help-articles/nextjs-prisma-client-dev-practices

次に、prisma/seed.tsを作成します

prisma/seed.ts
import { prisma } from '../lib/prisma'

const main = async () => {
  await prisma.task.createMany({
    data: [
      { title: 'sample task 1', done: true },
      { title: 'sample task 2', done: true },
      { title: 'sample task 3', done: false },
    ]
  })
}

main()
  .catch((e) => {
    console.error(e)
    process.exit(1)
  })
  .finally(async () => {
    await prisma.$disconnect()
  })

初期データを投入するためのコマンドを定義します
まず、必要なパッケージをインストールします

npm install -D ts-node @types/node

次に、package.jsonファイルにコマンドを定義します

package.json
 {
   "name": "next-graphql-todo-app",
   "version": "0.1.0",
   "private": true,
+  "prisma": {
+    "seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts"
+  },
   ...
 }

最後に、次のコマンドを実行します

npx prisma db seed
Environment variables loaded from .env
Running seed command `ts-node --compiler-options {"module":"CommonJS"} prisma/seed.ts` ...
prisma:query BEGIN
prisma:query INSERT INTO "public"."tasks" ("done","created_at","title","updated_at") VALUES ($1,$2,$3,$4), ($5,$6,$7,$8), ($9,$10,$11,$12)
prisma:query COMMIT

🌱  The seed command has been executed.

これで初期データを投入することができました

Prisma Studio

Prismaはデータベースのデータをブラウザで確認・操作できるツールであるPrimsa Studioを提供しています(便利!)

次のコマンドを実行すると、ブラウザでPrisma Studioが立ち上がります

npx prisma studio

tasksテーブルのデータを見ると、先ほど投入した初期データが保存されていることがわかります

img1

GraphQL APIを追加する

GraphQL APIのためのエンドポイントを追加して、GraphQLサーバーを起動します
GraphQLサーバーに必要なGraphQLスキーマresolverを定義していきます

手順

  • GraphQLスキーマを定義する
  • resolverを定義する
  • GraphQL APIのためのエンドポイントを追加する
  • Prismaを使ってクエリを実行する

GraphQLスキーマを定義する

まずは必要なパッケージをインストールしておきます

npm install graphql apollo-server-micro micro-cors

graphql/schema.tsファイルにGraphQLスキーマを定義します

graphql/schema.ts
import { gql } from 'apollo-server-micro'

export const typeDefs = gql`
  type Task {
    id: Int
    title: String
    done: Boolean
  }

  type Query {
    tasks: [Task]!
  }
`

ここでは以下の2つを定義しています

  • Taskオブジェクトとそのフィールド(カラム)
  • tasksクエリ

resolverを定義する

graphql/resolvers.tsファイルにクエリの実行結果を返す関数を定義します
今は仮データを直接記述しています
後からDBのデータを参照するように変更します

graphql/resolvers.ts
export const resolvers = {
  Query: {
    tasks: () => {
      return [
        {
          id: 1,
          title: 'sample task 1',
          done: true,
        },
        {
          id: 2,
          title: 'sample task 2',
          done: true,
        },
        {
          id: 3,
          title: 'sample task 3',
          done: false,
        },
      ]
    }
  }
}

エンドポイントを追加する

Next.jsのAPI Routesを利用してGraphQL APIのエンドポイントを追加します

pages/api/graphql.tsファイルを作成します

pages/api/graphql.ts
import { ApolloServer } from 'apollo-server-micro'
import { typeDefs } from '../../graphql/schema'
import { resolvers } from '../../graphql/resolvers'
import Cors from 'micro-cors'

const cors = Cors()

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

export default cors(async function handler(req, res) {
  if (req.method === 'OPTIONS') {
    res.end()
    return false
  }
  await startServer
  await apolloServer.createHandler({
    path: '/api/graphql',
  })(req, res)
})

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

new ApolloServerに先ほど定義したGraphQLスキーマとresolverを指定して、インスタンスを作成します
リクエストを受け取ったらstartServerapolloServer.createHandlerを実行して、GraphQLを扱えるようにします

ApolloServerについての詳細は次のページをご覧ください
https://www.apollographql.com/docs/apollo-server/api/apollo-server/

また、CORSを設定して、Apollo Studioを使えるようにしています
Apollo Studioについては後で触れますが、CORSの設定をしないとこのツールからGraphQLへのリクエストが失敗してしまいます

最後のconfigはbodyParserのデフォルト設定をfalseに変更し、GraphQLを扱えるようにしています

Apollo Studio

サーバを起動して http://localhost:3000/api/graphql を開くと、Apollo Studioが起動します

img2

このツールを使うとブラウザでGraphQLのスキーマ定義やクエリのレスポンスを確認することができます
tasksクエリを実行すると、resolverで定義したレスポンスが返ってくることがわかります

Prismaでクエリを実行する

resolverでPrismaを使うようにして、データベースからデータを取得するように変更します

まず、resolverがPrismaClientにアクセスし、データベースにクエリを送信できるようにするために、contextを作成します

graphql/context.ts
import { PrismaClient } from '@prisma/client'
import { prisma } from '../lib/prisma'

export type Context = {
  prisma: PrismaClient
}

export async function createContext(): Promise<Context> {
  return {
    prisma,
  }
}

ApolloServerにcontextを指定します

pages/api/graphql.ts
  import { ApolloServer } from 'apollo-server-micro'
  import { typeDefs } from '../../graphql/schema'
  import { resolvers } from '../../graphql/resolvers'
  import Cors from 'micro-cors'
+ import { createContext } from '../../graphql/context';

  const cors = Cors()

  const apolloServer = new ApolloServer({
    typeDefs,
    resolvers,
+   context: createContext,
  })
  const startServer = apolloServer.start()

  // ...
}

これでresolverがPrismaClientを使えるようになりました
resolverの関数がPrismaの実行結果を返すように変更します

graphql/resolvers.ts
export const resolvers = {
  Query: {
    tasks: (_parent, _args, ctx) => {
      return ctx.prisma.task.findMany()
    },
  },
}

ctxはcontextに指定した値を返します
ctx.prismaはPrismaClientであるので、Prismaを使ってデータベースからタスク一覧を取得することができます

_parent_argsはここでは使用していませんが、詳しくは次のページをご覧ください
https://www.apollographql.com/docs/apollo-server/data/resolvers/#resolver-arguments

Apollo Studioでtasksクエリを実行すると、Prismaを経由してPostgreSQLに保存されているデータを返すようになります

Nexusの導入

GraphQL Nexusを使ってGraphQLスキーマを定義するように変更していきます
Nexusを使うことには次のような利点があります

  • GraphQLのSDL(Schema Definition Language)ではなく、コードファースト(JavaScript/TypeScript)でスキーマを定義することができる
  • SDLではschemaとresolverの定義が分離していたが、Nexusでは2つを同じファイルで定義することができる
  • SDLファイルと型定義を自動生成してくれる
  • エディタで補完が効く

詳しくは次のページをご覧ください
https://nexusjs.org/docs/getting-started/why-nexus

Nexusでスキーマを定義する

graphql/types/Task.tsファイルを作成し、改めてNexusでスキーマ定義を記述します

graphql/types/Task.ts
import { objectType, extendType } from 'nexus'

export const Task = objectType({
  name: 'Task',
  definition(t) {
    t.nonNull.int('id')
    t.nonNull.string('title')
    t.nonNull.boolean('done')
  },
})

export const TasksQuery = extendType({
  type: 'Query',
  definition(t) {
    t.nonNull.list.field('tasks', {
      type: 'Task',
      resolve(_parent, _args, ctx) {
        return ctx.prisma.task.findMany()
      },
    })
  },
})

objectTypeでオブジェクトを定義し、extendTypeでQueryとMutationを定義します
ここではTaskオブジェクトと、tasksクエリを定義しています
resolvegraphql/resolvers.tsで書いていたように、クエリを実行する処理をここに書きます

オブジェクトごとに分けたファイルを集約するためのgraphql/types/index.tsも作っておきます

graphql/types/index.ts
export * from './Task'

Nexusに置き換える

まず、Nexusのライブラリをインストールします

npm install nexus

次に、graphql/schema.tsにはGraphQLのSDLでスキーマを定義していましたが、このファイルを丸ごと書き換えます

graphql/schema.ts
import { makeSchema } from 'nexus'
import { join } from 'path'
import * as types from './types'

export const schema = makeSchema({
  types,
  outputs: {
    typegen: join(process.cwd(), 'node_modules', '@types', 'nexus-typegen', 'index.d.ts'),
    schema: join(process.cwd(), 'graphql', 'schema.graphql'),
  },
  contextType: {
    export: 'Context',
    module: join(process.cwd(), 'graphql', 'context.ts'),
  },
})

NexusのmakeSchemaに以下を指定します

  • types: object typesの配列を指定する
  • outputs: Nexusが生成するファイルをどこに保存するかを指定する
    • typegen: 型定義ファイルをnode_modules/@types/nexus-typegen/index.d.tsに生成する設定
    • schema: GraphQL SDLファイルをgraphql/schema.graphqlに生成する設定
  • contextType: graphql/context.tsファイルを指定する

最後に、pages/api/graphql.tsを変更します

pages/api/graphql.ts
  import { ApolloServer } from 'apollo-server-micro'
- import { typeDefs } from '../../graphql/schema'
- import { resolvers } from '../../graphql/resolvers'
+ import { schema } from '../../graphql/schema'
  import Cors from 'micro-cors'
  import { createContext } from '../../graphql/context';

  const cors = Cors()

  const apolloServer = new ApolloServer({
-   typeDefs,
-   resolvers,
+   schema,
    context: createContext,
  })
  const startServer = apolloServer.start()

  // ...
}

スキーマ定義とresolverを指定していたところを、Nexusのschemaを指定するようにしました

Apollo Studioでtasksクエリを実行すると、Nexusで定義したスキーマとresolverに従ってレスポンスが返されるようになっていることを確認できます

これでGraphQLを使うための環境を構築することができました

ApolloClientインスタンスの作成

クライアント側でGraphQLクエリを実行するためにApolloClientを使用します
まず、次のパッケージをインストールします

npm install @apollo/client

次に、lib/apollo.tsでApolloClientのインスタンスを作成します

lib/apollo.ts
import { ApolloClient, InMemoryCache } from '@apollo/client'

const apolloClient = new ApolloClient({
  uri: 'http://localhost:3000/api/graphql',
  cache: new InMemoryCache(),
})

export default apolloClient

uriにはGraphQLエンドポイントを指定します
cacheにはInMemoryCacheのインスタンスを指定することで、クエリの結果をキャッシュに保存してくれるようになります

pages/_app.tsxを次のように変更して、各コンポーネントからApolloClientを使用してGraphQLクエリを送ることができるようにします

pages/_app.tsx
  import type { AppProps } from 'next/app'
+ import { ApolloProvider } from '@apollo/client'
+ import apolloClient from '../lib/apollo'

  function MyApp({ Component, pageProps }: AppProps) {
    return (
+     <ApolloProvider client={apolloClient}>
        <Component {...pageProps} />
+     </ApolloProvider>
    )
  }

  export default MyApp

シンプルTODOアプリの実装

useQueryでタスクデータを取得する

ここからやっとTODOアプリの実装になります
まず、タスク一覧を取得するところから始めます
tasksクエリを実行して結果を一覧表示するコードを以下に示します

pages/index.tsx
import type { NextPage } from 'next'
import Head from 'next/head'
import { gql, useQuery } from '@apollo/client'

const AllTasksQuery = gql`
  query {
    tasks {
      id
      title
      done
    }
  }
`

const Home: NextPage = () => {
  const { data, loading, error } = useQuery(AllTasksQuery)

  if (loading) return <p>Loading...</p>
  if (error) return <p>Error: {error.message}</p>

  return (
    <div>
      <Head>
        <title>Create Next App</title>
        <meta name='description' content='Generated by create next app' />
        <link rel='icon' href='/favicon.ico' />
      </Head>

      <main>
        <h1>SIMPLE TASK LIST</h1>
        <ul>
          {data.tasks.map((task) => (
            <li key={task.id}>
              <p>{task.title}</p>
            </li>
          ))}
        </ul>
      </main>
    </div>
  )
}

export default Home

img2

useQuery hookを使ってクエリを実行することができます
以下の返り値を受け取ります

  • loading: データ取得が完了したらtrueになるbooleanの値です
  • error: クエリ実行時にエラーが生じた場合に値が入ります
  • data: クエリのレスポンスが入ります

このように記述することでデータの取得・表示を行うことができます
また、データの操作を行うクエリは、後述するuseMutation hookを使います
データを扱う準備が整ったので、あとはTODOアプリの画面と機能を作っていくだけです

Chakra UIのセットアップ

UIライブラリとしてChakra UIを使用するのでインストールします

npm i @chakra-ui/react @emotion/react@^11 @emotion/styled@^11 framer-motion@^4 @chakra-ui/icons

ChakraProviderでAppコンポーネントを囲んでおきます

pages/_app.tsx
  import type { AppProps } from 'next/app'
  import { ApolloProvider } from '@apollo/client'
  import apolloClient from '../lib/apollo'
+ import { ChakraProvider } from '@chakra-ui/react'

  function MyApp({ Component, pageProps }: AppProps) {
    return (
      <ApolloProvider client={apolloClient}>
+       <ChakraProvider>
          <Component {...pageProps} />
+       </ChakraProvider>
      </ApolloProvider>
    )
  }

  export default MyApp

タスク一覧画面

前のキャプチャのタスク一覧画面をChakra UIで表示します
useQueryを使う方法は変わりませんのでコードは省略します

components/TaskList.tsx
components/TaskList.tsx
import { gql, useQuery } from '@apollo/client'
import { Checkbox, List, ListItem } from '@chakra-ui/react'

export const AllTasksQuery = gql`
  query {
    tasks {
      id
      title
      done
    }
  }
`

const TaskList: React.FC = () => {
  const { data, loading, error } = useQuery(AllTasksQuery)

  if (loading) return <p>Loading...</p>
   if (error) return <p>Error: {error.message}</p>

   return (
     <List>
       {data.tasks.map(task => (
         <ListItem key={task.id}>
           <Checkbox colorScheme='teal' isChecked={task.done}>
             {task.title}
             </Checkbox>
         </ListItem>
       ))}
     </List>
  )
}

export default TaskList
pages/index.tsx
pages/index.tsx
import type { NextPage } from 'next'
import Head from 'next/head'
import TaskList from '../components/TaskList'
import { Container, Heading, Stack } from '@chakra-ui/react'

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

      <main>
        <Container my='32px'>
          <Stack spacing='32px'>
            <Heading>TASK LIST</Heading>
            <TaskList />
          </Stack>
        </Container>
      </main>
    </div>
  )
}

export default Home

タスク追加フォーム

タスク追加フォームを表示して、タスクの追加を行えるようにします
そのために必要な、タスクの作成を行うMutationをgraphql/Task.tsに新しく定義します

graphql/Task.ts
import { objectType, extendType, stringArg, nonNull } from 'nexus'

// ...

export const CreateTaskMutation = extendType({
  type: 'Mutation',
  definition(t) {
    t.nonNull.field('createTask', {
      type: 'Task',
      args: {
        title: nonNull(stringArg()),
      },
      resolve(_parent, args, ctx) {
        return ctx.prisma.task.create({
          data: {
            title: args.title
          }
        })
      }
    })
  }
})

クライアントでMutationを実行するには、useMutation hookを使います
タスクの作成を行うmutationは以下のように書くことで実行することができます

useMutationの使い方
import { gql, useMutation } from '@apollo/client'

const CreateTaskMutaiton = gql`
  mutation CreateTask($title: String!) {
    createTask(title: $title) {
      id
      title
      done
    }
  }
`

const [createTask, { error }] = useMutation(CreateTaskMutation)

// mutationの実行
createTask({
  variables: {
    title: title,
  },
})

useMutationは2つのタプルを返します
1つ目のcreateTaskはmutation functionであり、これを呼び出すことで実際にmutationを実行します
2つ目はオブジェクトで、errorやloadingなどのステータスを表すデータを返します

createTaskを呼び出すときはvariablesに引数を指定することができます

createTask mutationを実行すると新しいタスクが作成されます
しかし、このときタスク一覧結果には表示されません
mutationを実行したあと、特定のqueryを再取得するには、次のようにuseMutationの第2引数にrefetchQueriesを指定します

import { AllTasksQuery } from './TaskList'

const [createTask, { error }] = useMutation(CreateTaskMutation, {
  refetchQueries: [AllTasksQuery],
})
components/TaskAddForm.tsx

フォームのライブラリであるformikを使うのでインストールしておきます

npm install formik --save
components/TaskAddForm.tsx
import { Formik, Field, Form } from 'formik'
import { Stack, FormControl, Input, Button } from '@chakra-ui/react'
import { gql, useMutation } from '@apollo/client'
import { AllTasksQuery } from './TaskList'

const CreateTaskMutaiton = gql`
  mutation CreateTask($title: String!) {
    createTask(title: $title) {
      id
      title
      done
    }
  }
`

const TaskAddForm: React.FC = () => {
  const [createTask, { error }] = useMutation(CreateTaskMutaiton, {
    refetchQueries: [AllTasksQuery],
  })

  const handleSubmit = (title: string, resetForm: () => void) => {
    if (!title) return
    createTask({
      variables: {
        title: title,
      },
    })
    resetForm()
  }

  if (error) return <p>Error: {error.message}</p>

  return (
    <Formik
      initialValues={{ title: '' }}
      onSubmit={(value, actions) => handleSubmit(value.title, actions.resetForm)}
    >
      <Form>
        <Stack direction='row'>
          <Field name='title'>
            {({ field }) => (
              <FormControl>
                <Input {...field} id='title' type='text' placeholder='Add task' />
              </FormControl>
            )}
          </Field>
          <Button colorScheme='teal' type='submit'>
            Submit
          </Button>
        </Stack>
      </Form>
    </Formik>
  )
}

export default TaskAddForm
pages/index.tsx
pages/index.tsx
  import type { NextPage } from 'next'
  import Head from 'next/head'
  import TaskList from '../components/TaskList'
  import { Container, Heading, Stack } from '@chakra-ui/react'
+ import TaskAddForm from '../components/TaskAddForm'

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

        <main>
          <Container my='32px'>
            <Stack spacing='32px'>
              <Heading>TASK LIST</Heading>
+             <TaskAddForm />
              <TaskList />
            </Stack>
          </Container>
       </main>
      </div>
    )
  }

  export default Home

gif1

完成品

残りタスクの削除機能、完了チェック機能を加えたら完成です
query, mutationの実行方法は変わらないためコードは省略します

最終的なコードは次のリポジトリにありますので、もし気になればご参照ください
https://github.com/youichiro/next-graphql-simple-todo-app

参考にしたサイト

https://www.prisma.io/blog/fullstack-nextjs-graphql-prisma-oklidw1rhw
https://www.prisma.io/blog/fullstack-nextjs-graphql-prisma-2-fwpc6ds155

Discussion