Agent Grow Tech Notes

TypeScript & GraphQL でToDoアプリを開発する #3

に公開

⬅️前回の記事はこちら

https://zenn.dev/agent_grow/articles/477f98f021fd2d

Apollo Server & Apollo Client の起動

⭐️Apollo Serverの起動

backend/にsrc/index.tsを作成

index.ts
import express from 'express';                                 // Expressを読み込み
import http from 'http';                                       // HTTPサーバーモジュール ExpressをHTTPサーバーとして起動するために読み込み
import { ApolloServer } from '@apollo/server';                 // GraphQLサーバーを作るためのApollo Serverのクラス
import { expressMiddleware } from '@apollo/server/express4';   // Apollo ServerをExpressと統合するためのミドルウェア
import { typeDefs } from './graphql/typeDefs';                   // GraphQLのスキーマ定義(typeDefs)を読み込み
import { resolvers } from './graphql/resolvers';               // GraphQLのロジック(resolvers)を読み込み
import cors from 'cors';                                       // CORSを有効にするミドルウェアを読み込み

export async function startApolloServer() {      // 非同期関数startApolloServerを定義してエクスポート
    const app = express();                       // Expressアプリケーションのインスタンス
    const httpServer = http.createServer(app);   // そのExpressを使ってHTTPサーバーを作成

    const server = new ApolloServer({   // ApolloServerにGraphQLのスキーマとロジックを渡して、インスタンスを作成
        typeDefs,
        resolvers,
    });

    await server.start();               // ApolloServerの準備(スキーマ検証など)をおこなう

    app.use(                        // ミドルウェアを登録するメソッド
        '/graphql',                 // /graphqlに来たリクエストに対して以下を順番に処理する
        cors(),                     // CORSを許可
        express.json(),             // JSON形式のリクエストボディをパース
        expressMiddleware(server)   // GraphQLリクエストをApollo Serverに渡す
    );

    const PORT = 4001;                               // ポート4001でHTTPサーバーを起動
    await new Promise<void>((resolve) =>
        httpServer.listen({ port: PORT }, resolve)   // 非同期処理で、サーバーが立ち上がるのを待機
    );
    console.log(`🚀 http://localhost:${PORT}/graphql でサーバーの準備完了!`);
}

backend/src/にgraphql/typeDefs.tsを作成

typeDefs.ts
import { gql } from 'graphql-tag'

export const typeDefs = gql`  // typeDefsという変数にGraphQLスキーマを定義してエクスポートする
  type Todo {   // Todoというデータ型(オブジェクト)を定義 
    id: Int!
    title: String!
    completed: Boolean!
  }

  type Query {   // クエリ(読み取り)操作を定義
    getTodos: [Todo!]!
  }

  type Mutation {   // ミューテーション(更新系)操作を定義                 
    createTodo(title: String!): Todo!
    updateTodo(id: Int!, title: String, completed: Boolean): Todo!
    deleteTodo(id: Int!): Boolean!
  }
`

backend/src/graphql/にresolvers.tsを作成

resolvers.ts
import { PrismaClient, Todo } from '@prisma/client'

// PrismaClient のインスタンスを作成(以降、prisma オブジェクトから DB を操作できる)
const prisma = new PrismaClient()

// GraphQL のリゾルバを定義・エクスポート
export const resolvers = {
    // Query(データの取得系)リゾルバ定義
    Query: {
        // Todo一覧を取得するクエリ
        getTodos: async () => {
            return prisma.todo.findMany() // Prisma 経由で Todo 全件を取得して返す
        },
    },

    // Mutation(データの変更系)リゾルバ定義
    Mutation: {
        // 新しい Todo を作成するミューテーション
        createTodo: async (_parent: unknown, args: { title: string }) => {
            return prisma.todo.create({
                data: { 
                    title: args.title,       // 引数からタイトルを受け取り
                    completed: false         // 初期状態では未完了に設定
                },
            })
        },

        // 既存の Todo を更新するミューテーション
        updateTodo: async (_parent: unknown, args: { id: number; title?: string; completed?: boolean }) => {
            const { id, title, completed } = args

            const dataToUpdate: Partial<Todo> = {} // 更新対象フィールドを格納するオブジェクト
            if (title !== undefined) dataToUpdate.title = title         // title が渡されていれば反映
            if (completed !== undefined) dataToUpdate.completed = completed // completed も同様

            return prisma.todo.update({
                where: { id },                // id で対象レコードを特定し
                data: dataToUpdate,           // 指定されたフィールドのみ更新
            })
        },

        // Todo を削除するミューテーション
        deleteTodo: async (_parent: unknown, args: { id: number }) => {
            await prisma.todo.delete({
                where: { id: args.id },       // id で対象レコードを特定し削除
            })
            return true // 削除が成功したら true を返す
        },
    },
}

backend/src/にserver.tsを作成

server.ts
import { startApolloServer } from './index'

startApolloServer()   // startApolloServerを実行

backend/package.jsonのscriptセクションでdevスクリプトを定義

package.json
// 修正前
"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
},
package.json
// 修正後
"scripts": {
    "dev": "ts-node-dev --respawn --transpile-only src/server.ts",   // devスクリプトを定義
    "test": "echo \"Error: no test specified\" && exit 1"
}

backend/で npm run dev を実行

% npm run dev

index.tsのconsole.log()の内容が表示されたら成功

> backend@1.0.0 dev
> ts-node-dev --respawn --transpile-only src/server.ts

[INFO] 09:24:59 ts-node-dev ver. 2.0.0 (using ts-node ver. 10.9.2, typescript ver. 5.8.3)
🚀 http://localhost:4001/graphql でサーバーの準備完了!

http://localhost:4001/graphql にアクセスすると、SANDBOX(Postmanみたいなもの)が表示される

🚀Apollo Serverの起動が完了!!!

#3のおわりに

Apollo Server の起動 お疲れさまでした。
次回は Apollo Client の起動 をします。
https://zenn.dev/agent_grow/articles/c5a622f1a98522

〜GraphQLとは、RESTとの違い〜

APIのためのクエリ言語であり、「欲しいデータだけを、1回のリクエストで取得できる」仕組みを提供します。

◼️具体例
「ブログサイトでユーザーのプロフィールと、その人が過去に書いた記事のタイトル一覧を表示する」
という画面を作りたい時

  • RESTの場合
     下記のように2回リクエストを送る必要があります。
     そして、レスポンスに不要な情報が含まれる可能性もあります。
GET /users/1
GET /users/1/posts
  • GraphQLの場合
     1回のリクエストで、必要な情報だけを取得できます。
query {
  user(id: 1) {
    name
    posts {
      title
    }
  }
}

GraphQLは、必要なデータを柔軟に効率よく取得できる仕組みであり、RESTに比べてクライアント主導の設計がしやすくなります。特にモバイルアプリや複雑なUIを持つWebアプリと非常に相性が良く、開発の効率化に加え、パフォーマンス改善にもつながります。

Agent Grow Tech Notes
Agent Grow Tech Notes

Discussion