GraphQL を使った ToDo アプリ - バックエンド編

May 03, 2023

はじめに

GraphQL について 一通り学んだ ところで、実際のユースケースをより深く理解すべく、簡単な ToDo アプリを作りたいと思います。今回はデータストアとして DynamoDB テーブルを利用し、具体的には以下のデータを格納します。

フィールド 説明
id ToDo エントリの ID
title タイトル
description 詳細
createDate 作成日

これらの情報を参照/作成/更新/削除できるような Web アプリの制作を行い、GraphQL への理解をより深めることを目的とします。なお、本エントリはバックエンド、つまり GraphQL サーバー側にフォーカスしています。

プロジェクトディレクトリの作成

まずはプロジェクトディレクトリを作成します。今回は backend/ に GraphQL サーバー、frontend/ にフロントのアプリを配置するような構成を取ります。

$ mkdir todo-app
$ mkdir backend frontend

環境構築

それでは実際にバックエンドの開発に取り掛かります。
まずは必要なライブラリのインストールからです。なお aws-sdk は DynamoDB テーブルに対する操作、uuid については id の生成に利用します。

$ cd backend frontend
$ npm init -y
$ npm install graphql apollo-server aws-sdk uuid

次に必要なディレクトリ、ファイルを作成してきます。

$ mkdir resolver model
$ touch index.js schema.js model/todo.js resolver/{Query.js,Mutation.js}

以上で完了です。backend/ 配下は以下のようなディレクトリ構造となります。

$ tree -L 2 -I node_modules
.
├── index.js
├── model
│   └── todo.js
├── package-lock.json
├── package.json
├── resolver
│   ├── Mutation.js
│   └── Query.js
└── schema.js

2 directories, 7 files

スキーマの定義

スキーマの定義を行います。今回は ToDo の CRUD 処理ができることを目的とするため、それに沿ったスキーマを書いていきます。なお、参照については LIST のみの実装とします。

backend/schema.js
const {gql} = require("apollo-server")

const typeDefs = gql`
  type Query {
    listTodos: [Todo!]!
  }

  type Mutation {
    createTodo(data: CreateTodoInput!): Todo!
    updateTodo(id: ID!, data: UpdateTodoInput!): Todo!
    deleteTodo(id: ID!): Todo!
  }

  input CreateTodoInput {
    title: String!
    description: String!
    createDate: String!
  }

  input UpdateTodoInput {
    title: String!
    description: String!
    createDate: String!
  }

  type Todo {
    id: ID!
    title: String!
    description: String!
    createDate: String!
  }
`;

module.exports = typeDefs;

モデルの作成

以下のように Todo モデルを作成し、DynamoDB への操作はクラスのメソッドとして行うことにします。

backend/model/todo.js
const AWS = require("aws-sdk");

AWS.config.update({
  region: "us-west-2"
});
const docClient = new AWS.DynamoDB.DocumentClient();
const tableName = "GraphQLToDoAppTable";


class Todo {
  constructor(id, title, description, createDate) {
    this.id = id;
    this.title = title;
    this.description = description;
    this.createDate = createDate;
  }

  static async listTodos() {
    let todoList = [];
    let params = { TableName: tableName }

    while (true) {
      const result = await docClient.scan(params).promise();
      if (result.Items) {
        result.Items.forEach((item) => {
          const {id, title, description, createDate} = item;
          todoList.push(new Todo(id, title, description, createDate));
        });
      }
      if (result.LastEvaluatedKey) {
        params.ExclusiveStartKey = result.LastEvaluatedKey;
      } else {
        break;
      }
    }
    return todoList;
  }

  static async deleteTodo(id) {
    const params = {
      TableName: tableName,
      Key: {
        "id": id
      },
      ReturnValues: "ALL_OLD"
    };
    const response = await docClient.delete(params).promise();
    return response.Attributes;
  }
  
  async createTodo() {
    const params = {
      TableName: tableName,
      Item: {
        id: this.id,
        title: this.title,
        description: this.description,
        createDate: this.createDate
      }
    };
    await docClient.put(params).promise();
  }

  async updateTodo() {
    const params = {
      TableName: tableName,
      Key: {
        "id": this.id
      },
      UpdateExpression: "set title = :title, description = :desc, createDate = :cd",
      ExpressionAttributeValues: {
        ":title": this.title,
        ":desc": this.description,
        ":cd": this.createDate
      }
    };
    const response = await docClient.update(params).promise();
  }
}

module.exports = Todo;

リゾルバの作成

次に以下のようにリゾルバを作成します。Query 及び Mutation のリゾルバはそれぞれ resolver/Query.js 及び resolver/Mutation.js に配置します。

backend/resolver/Query.js
const Todo = require("../model/todo");

const Query = {
  async listTodos(parent, args, info) {
    const todos = Todo.listTodos();
    return todos;
  }
}

module.exports = Query;
backend/resolver/Mutation.js
const uuid = require('uuid');
const Todo = require("../model/todo");

const Mutation = {
  async createTodo(parent, args, info) {
    const {title, description, createDate} = args.data;
    const id = uuid.v4();
    const todo = new Todo(id, title, description, createDate);
    await todo.createTodo();
    return todo;
  },
  async updateTodo(parent, args, info) {
    const id = args.id;
    const {title, description, createDate} = args.data;
    const todo = new Todo(id, title, description, createDate);
    await todo.updateTodo();
    return todo;
  },
  async deleteTodo(parent, args, info) {
    const id = args.id;
    const todo = await Todo.deleteTodo(id);
    return new Todo(todo.id, todo.title, todo.description, todo.createDate);
  }
}

module.exports = Mutation;

Apollo サーバーの起動

後は Apollo サーバーを起動するための処理を書いて完了です。

backend/index.js
const {ApolloServer} = require("apollo-server");
const Query = require("./resolver/Query");
const Mutation = require("./resolver/Mutation");
const typeDefs = require("./schema");

const server = new ApolloServer({
  typeDefs: typeDefs,
  resolvers: {
    Query,
    Mutation
  }
});

server.listen().then(({ url }) => {
  console.log(`Server ready at ${url}`);
});

動作の確認

ここまでできたらサーバーを起動し、GraphQL Playground で動作確認を行います。

$ node index.js 
Server ready at http://localhost:4000/

まずは ToDo の作成から。

f:id:shiro_kochi:2018××××××××:plain:w100:left

実際に DynamoDB テーブルにもデータが作成されていることがわかります。

f:id:shiro_kochi:2018××××××××:plain:w100:left

次に、上記 ToDo の description を更新してみましょう。

f:id:shiro_kochi:2018××××××××:plain:w100:left

こちらも DynamoDB テーブルから更新が確認できます。

f:id:shiro_kochi:2018××××××××:plain:w100:left

それでは削除してみます。

f:id:shiro_kochi:2018××××××××:plain:w100:left

削除されたかどうか、今度は listTodos で確認してみましょう。

f:id:shiro_kochi:2018××××××××:plain:w100:left

ちゃんと削除されていそうですね。なお、実際に ToDo が存在する場合は以下のように ToDo リストを返してくれます。

f:id:shiro_kochi:2018××××××××:plain:w100:left

おわりに

如何でしたでしょうか。個人的には実際に IDE 環境を使って動作確認が行えるため、開発がスムーズに行えた気がしています。
次回は上記フロントエンド UI を作っていこうと思います。


 © 2023, Dealing with Ambiguity