【Docker / Nuxt.js / TypeScript / Expressで作るタスク管理アプリ】PrismaでCRUDをやってみよう！

## はじめに
今回はDockerとNuxt.jsとTypeScriptとExpressを使ってタスク管理アプリを作成していきます。
こんな感じのアプリです。

![スクリーンショット 2021-08-30 18.32.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/41a37e8f-e6bb-a0a1-2e40-521db221c8c5.png)


前回書いた記事のTypeScriptバージョンです。
JavaScriptを勉強中の方は以下の記事を見てください。

https://qiita.com/Ryo9597/items/780961917d28d7cd7fb1

## 対象読者
<ul>
<li>Dockerを使ったことがない方</li>
<li>Nuxt.jsとExpressで何か作ってみたい方</li>
<li>TypeScriptを使ったことがない方</li>
<li>TypeScriptのORMであるPrismaを使ってみたい方</li>
</ul>

## 完成コード

https://github.com/ssk9597/Nuxt-TypeScript-Express-Task

## それでは一緒に作っていきましょう！
### Githubからクローン
Dockerから解説となると長くなるので省略します。

https://github.com/ssk9597/Docker-Nuxt-TypeScript-Express-Prisma

上記のファイルで環境構築が可能です。

以下の記事で具体的なやり方が書いてありますのでこちらを参考にしてください。

https://qiita.com/Ryo9597/items/875a1604029e1af9ea16

### `api/prisma/schema.prisma`を修正する

```diff_html:api/prisma/schema.prisma
datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

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

+ model Task {
+   id         Int      @id @default(autoincrement())
+   content    String
+   isFinished Boolean  @default(false)
+   createdAt  DateTime @default(now())
+ }
```

### タスク追加機能の作成

#### フロントエンド

```vue:frontend/pages/index.vue
<template>
  <div>
    <h2>Task App</h2>
    <br />
    <br />
    <h3>ADD_TASK</h3>
    <br />
    <input type="text" v-model="content" />
    <button @click="createTask()">追加</button>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref, useAsync, useContext } from '@nuxtjs/composition-api';
import axios from '@nuxtjs/axios';

type TaskType = {
  id?: number;
  content: string;
  isFinished: boolean;
  createdAt?: string;
};

export default defineComponent({
  setup() {
    // axios
    const { $axios } = useContext();

    // data
    const content = ref<string>('');
    const task = ref<TaskType>({
      content: content,
      isFinished: false,
    });

    // methods
    const createTask = async () => {
      window.location.href = 'http://localhost:3000';
      await $axios.$post('/api/tasks/store', {
        task: task.value,
      });
    };

    return {
      // data
      content,
      task,
      // methods
      createTask,
    };
  },
});
</script>
```

#### バックエンド
##### app.ts
```diff_typescript:api/src/app.ts
// express
import express from 'express';

// controller_file
import indexController from './controller/IndexController';
+ import taskController from './controller/TaskController';

// app
const app = express();

// bodyParser
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// cors
import cors from 'cors';
app.use(cors());

// routing
app.use('/api', indexController);
+ app.use('/api/tasks', taskController);

export default app;
```

##### TaskController.ts
```diff_typescript:api/src/controller/TaskController.ts
import { PrismaClient } from '@prisma/client';
import { Router, Request, Response } from 'express';

const prisma = new PrismaClient();
const router = Router();

// POST /api/tasks/store
router.post('/store', async (req: Request, res: Response) => {
  await prisma.task.create({
    data: {
      content: req.body.task.content,
      isFinished: req.body.task.isFinished,
    },
  });
});

export default router;
```

### タスク一覧機能を作成
#### フロントエンド
```diff_vue:frontend/pages/index.vue
<template>
  <div>
    <h2>Task App</h2>
    <br />
    <br />
+   <h3>SHOW_TASK</h3>
+   <br />
+   <table>
+     <thead>
+       <tr>
+         <th>ID</th>
+         <th>CONTENT</th>
+         <th>STATUS</th>
+         <th>DELETE</th>
+       </tr>
+     </thead>
+     <tbody>
+       <tr v-for="(task, index) in tasks" :key="index">
+         <td>{{ index + 1 }}</td>
+         <td>{{ task.content }}</td>
+         <td v-if="task.isFinished === true">
+           <button>完了</button>
+         </td>
+         <td v-else>
+           <button>作業中</button>
+         </td>
+         <td><button>削除</button></td>
+       </tr>
+     </tbody>
+   </table>
+   <br />
    <h3>ADD_TASK</h3>
    <br />
    <input type="text" v-model="content" />
    <button @click="createTask()">追加</button>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref, useAsync, useContext } from '@nuxtjs/composition-api';
import axios from '@nuxtjs/axios';

type TaskType = {
  id?: number;
  content: string;
  isFinished: boolean;
  createdAt?: string;
};

export default defineComponent({
  setup() {
    // axios
    const { $axios } = useContext();

    // asyncData
+   useAsync(async () => {
+     const result = await $axios.$get('/api/tasks');
+     tasks.value = result.tasks;
+   });

    // data
    const content = ref<string>('');
    const task = ref<TaskType>({
      content: content,
      isFinished: false,
    });
+   const tasks = ref<TaskType[]>();

    // methods
    const createTask = async () => {
      window.location.href = 'http://localhost:3000';
      await $axios.$post('/api/tasks/store', {
        task: task.value,
      });
    };

    return {
      // data
      content,
      task,
      tasks,
      // methods
      createTask,
    };
  },
});
</script>
```

#### バックエンド
```diff_typescript:api/src/controller/TaskController.ts
import { PrismaClient } from '@prisma/client';
import { Router, Request, Response } from 'express';

const prisma = new PrismaClient();
const router = Router();

+ // GET /api/tasks
+ router.get('/', async (req: Request, res: Response) => {
+   const tasks = await prisma.task.findMany();
+   res.status(200).json({ tasks });
+ });

// POST /api/tasks/store
router.post('/store', async (req: Request, res: Response) => {
  await prisma.task.create({
    data: {
      content: req.body.task.content,
      isFinished: req.body.task.isFinished,
    },
  });
});

export default router;
```

### タスク削除機能を作成
#### フロントエンド
```diff_vue:frontend/pages/index.vue
<template>
  <div>
    <h2>Task App</h2>
    <br />
    <br />
    <h3>SHOW_TASK</h3>
    <br />
    <table>
      <thead>
        <tr>
          <th>ID</th>
          <th>CONTENT</th>
          <th>STATUS</th>
          <th>DELETE</th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="(task, index) in tasks" :key="index">
          <td>{{ index + 1 }}</td>
          <td>{{ task.content }}</td>
          <td v-if="task.isFinished === true">
            <button>完了</button>
          </td>
          <td v-else>
            <button>作業中</button>
          </td>
+         <td><button @click="deleteTask(task.id, task.isFinished)">削除</button></td>
        </tr>
      </tbody>
    </table>
    <br />
    <h3>ADD_TASK</h3>
    <br />
    <input type="text" v-model="content" />
    <button @click="createTask()">追加</button>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref, useAsync, useContext } from '@nuxtjs/composition-api';
import axios from '@nuxtjs/axios';

type TaskType = {
  id?: number;
  content: string;
  isFinished: boolean;
  createdAt?: string;
};

export default defineComponent({
  setup() {
    // axios
    const { $axios } = useContext();

    // asyncData
    useAsync(async () => {
      const result = await $axios.$get('/api/tasks');
      tasks.value = result.tasks;
    });

    // data
    const content = ref<string>('');
    const task = ref<TaskType>({
      content: content,
      isFinished: false,
    });
    const tasks = ref<TaskType[]>();

    // methods
    const createTask = async () => {
      window.location.href = 'http://localhost:3000';
      await $axios.$post('/api/tasks/store', {
        task: task.value,
      });
    };

+   const deleteTask = async (id: number) => {
+     window.location.href = 'http://localhost:3000';
+     await $axios.$post('/api/tasks/delete', {
+       id: id,
+     });
+   };

    return {
      // data
      content,
      task,
      tasks,
      // methods
      createTask,
+     deleteTask,
    };
  },
});
</script>
```

#### バックエンド
```diff_typescript:api/src/controller/TaskController.ts
import { PrismaClient } from '@prisma/client';
import { Router, Request, Response } from 'express';

const prisma = new PrismaClient();
const router = Router();

// GET /api/tasks
router.get('/', async (req: Request, res: Response) => {
  const tasks = await prisma.task.findMany();
  res.status(200).json({ tasks });
});

// POST /api/tasks/store
router.post('/store', async (req: Request, res: Response) => {
  await prisma.task.create({
    data: {
      content: req.body.task.content,
      isFinished: req.body.task.isFinished,
    },
  });
});

+ // POST /api/tasks/delete
+ router.post('/delete', async (req: Request, res: Response) => {
+   const id: number = req.body.id;
+   await prisma.task.delete({
+     where: {
+       id: id,
+     },
+   });
+ });

export default router;
```

### タスク更新機能を作成
#### フロントエンド
```diff_vue:frontend/pages/index.vue
<template>
  <div>
    <h2>Task App</h2>
    <br />
    <br />

    <h3>SHOW_TASK</h3>
    <br />

    <table>
      <thead>
        <tr>
          <th>ID</th>
          <th>CONTENT</th>
          <th>STATUS</th>
          <th>DELETE</th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="(task, index) in tasks" :key="index">
          <td>{{ index + 1 }}</td>
          <td>{{ task.content }}</td>
          <td v-if="task.isFinished === true">
            <button @click="updateTask(task.id, task.isFinished)">完了</button>
          </td>
          <td v-else>
+           <button @click="updateTask(task.id, task.isFinished)">作業中</button>
          </td>
+         <td><button @click="deleteTask(task.id, task.isFinished)">削除</button></td>
        </tr>
      </tbody>
    </table>
    <br />

    <h3>ADD_TASK</h3>
    <br />
    <input type="text" v-model="content" />
    <button @click="createTask()">追加</button>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref, useAsync, useContext } from '@nuxtjs/composition-api';
import axios from '@nuxtjs/axios';

type TaskType = {
  id?: number;
  content: string;
  isFinished: boolean;
  createdAt?: string;
};

export default defineComponent({
  setup() {
    // axios
    const { $axios } = useContext();

    // asyncData
    useAsync(async () => {
      const result = await $axios.$get('/api/tasks');
      tasks.value = result.tasks;
    });

    // data
    const content = ref<string>('');
    const task = ref<TaskType>({
      content: content,
      isFinished: false,
    });
    const tasks = ref<TaskType[]>();

    // methods
    const createTask = async () => {
      window.location.href = 'http://localhost:3000';
      await $axios.$post('/api/tasks/store', {
        task: task.value,
      });
    };

    const deleteTask = async (id: number) => {
      window.location.href = 'http://localhost:3000';
      await $axios.$post('/api/tasks/delete', {
        id: id,
      });
    };

+   const updateTask = async (id: number, isFinished: boolean) => {
+     window.location.href = 'http://localhost:3000';
+     await $axios.$post('/api/tasks/update', {
+       id: id,
+       isFinished: isFinished,
+     });
+   };

    return {
      // data
      content,
      task,
      tasks,
      // methods
      createTask,
      deleteTask,
+     updateTask,
    };
  },
});
</script>
```

#### バックエンド
```diff_typescript:api/src/controller/TaskController.ts
import { PrismaClient } from '@prisma/client';
import { Router, Request, Response } from 'express';

const prisma = new PrismaClient();
const router = Router();

// GET /api/tasks
router.get('/', async (req: Request, res: Response) => {
  const tasks = await prisma.task.findMany();
  res.status(200).json({ tasks });
});

// POST /api/tasks/store
router.post('/store', async (req: Request, res: Response) => {
  await prisma.task.create({
    data: {
      content: req.body.task.content,
      isFinished: req.body.task.isFinished,
    },
  });
});

// POST /api/tasks/delete
router.post('/delete', async (req: Request, res: Response) => {
  const id: number = req.body.id;
  await prisma.task.delete({
    where: {
      id: id,
    },
  });
});

+ // POST /api/tasks/update
+ router.post('/update', async (req: Request, res: Response) => {
+   const id: number = req.body.id;
+   const isFinished: boolean = req.body.isFinished;
+
+   await prisma.task.update({
+     where: { id: id },
+     data: { isFinished: !isFinished },
+   });
+ });

export default router;
```

## 終わりに
これで、`TypeScript`と`Composition-API`、`Prisma`の導入ができました。
次はもう少しレベルを上げたものを作っていくことにします！