Node.js+MySQL+Dockerを使用したAPIの作成


## はじめに
Node.js+MySQL+Dockerを使用したAPIの作成をしていきます。

記事を書くにあたり、trelloクローンアプリの作成を最終目標としています。
その過程で、バックエンド構築の際に今回の記事の内容で作成してみました。

作成にあたり主に下記の記事を参考にしております。

>【Docker】DockerにMySQL環境を構築する（M1 Mac 対応）
> https://midorigame-jo.com/docker-mysql/
> DockerでMySQL・Node.jsコンテナを用いたToDoリストアプリを作成する
> https://qiita.com/niisan1ban/items/f70eb0ed891568f71f9b
> 次世代の Node.js ORM 、Prisma をサクッと入門する
> https://zenn.dev/pyhrinezumi/articles/431be604f9ad50
> 【Web API】Talend API Testerを使ってみた
> https://www.isoroot.jp/blog/4241/


## 環境
MacBookAir m1
VSCode
Node.js ver 18
MySQL ver 8.0.33
TypeScript
Prisma
Docker

#### 1.環境構築
はじめに、docker-composeを作成します。
内容は下記の設定としております。

````docker-compose.yml
services:
  db:
    image: mysql:8.0.33
    #Macbook m1を使用しているためplantformを指定
    platform: linux/amd64
    container_name: mysql
    command: --default-authentication-plugin=mysql_native_password
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_USER: ユーザ名
      MYSQL_PASSWORD: パスワード
      MYSQL_DATABASE: trello
      TZ: "Asia/Tokyo"
    volumes:
      - db-data:/var/lib/mysql
    ports:
      - 3307:3306

  backend:
    image: node:18-slim
    volumes:
      - ./backend:/usr/src/app
    working_dir: /usr/src/app
    command: bash -c "npm install && npm start"
    ports:
      - 3000:3000
    depends_on:
      - db
    #URLはlocalhostではなく、docker-composeで指定しているservicesのdbを指定
    environment:
      DATABASE_URL: mysql://ユーザー名:パスワード@db:3306/trello

volumes:
  db-data:
````
:::note info
手間を省くため、docker-compose上でDBアクセス用のユーザの払い出しとデータベースの設定をしました。
それに伴いURLも変更しております。

使用している環境により設定する内容が変わるため、詳細は公式リファレンスを確認するようにしてください。
筆者はMacBookM1だったため、環境構築に苦労しました、、、
:::

上記をプロジェクトのルートフォルダへ配置し、ターミナルで下記コマンドを実行します。

````npm
docker-compose up -d
````
これにより、MysqlとNode.jsの環境構築が完了しました。

#### 2.テーブル設定

では、次にデータベースのテーブル設定を行います。
今回は、Prismaを使用してマイグレーションを行います。
Node.jsのORMはいくつかありますが、TypeScriptと親和性があり、使用しやすいイメージをがあったため、今回採用してみました。

ディレクトリをbackendフォルダに移動し、下記コマンドを順番に実行します。
````npm
npm init -y
npm install prisma --save-dev
npx prisma init --datasource-provider mysql
````
実行後、自動的にbackendフォルダ直下に.envファイルとPrismaフォルダとschema.prismaが作成されます。
ここでテーブル、カラム設定を行うことができます。
今回は下記のように設定を行います。

````schema.prisma
generator client {
  provider = "prisma-client-js"
}

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

model Boards {
  id            Int     @id @default(autoincrement())
  name          String
  board_order   Int
}
````
.envファイルには、マイグレーション用のURLが記述してあります。
Dockerの環境に合わせ、下記のように設定しております。
````.env
DATABASE_URL="mysql://root:root@0.0.0.0:3307/trello"
````
:::note warn
localhostではなく、0.0.0.0を設定しています。
ポートはDocker-composeで設定した値です。
:::

設定完了後、ターミナルでbackend下で下記コマンドを入力します。
````npm
npx prisma migrate dev --name init
````

完了すると、テーブルとカラムの設定が自動で行われます。
Dcoker DeskTopで確認してみましょう。
![スクリーンショット 2023-07-11 17.26.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3495347/42654827-7a4c-dddd-9de0-308970e75aa4.png)

:::note info

Prismaでは、下記コマンドを実行するとクラインとツールが起動し、DBの操作も行えるため非常に便利です。
````npm
npx prisma studio
````
![スクリーンショット 2023-07-11 17.29.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3495347/7c778683-2ade-b743-4140-81314edff3da.png)
:::


以上でデータベースの設定が完了です。

#### 3.Node.jsでAPI構築

Node.jsのexpressを使用してAPIの作成をしていきます。
データベースの接続は本来であればPrismaをそのまま使用していきたいですが、MacBookM1でDockr上のMysqlをlinux/amd64で起動しているとエラーが出てしまい、うまく解消ができませんでした。
そのためmysql2を使用していきます。
まず、下記コマンドを実行していきます。
````npm
npm install express
npm install --save-dev typescript ts-node @types/node @types/express
npm install -D nodemon
npm install --save doting
npm install --save mysql2
npm install cors --save
````
次にtsconfig.jsonを作成し、下記内容とします。
````tsconfig.json
{
    "compilerOptions": {
      "target": "esnext",
      "module": "commonjs",
      "esModuleInterop": true,
      "forceConsistentCasingInFileNames": true,
      "strict": true,
      "skipLibCheck": true,
      "sourceMap": true,
      "outDir": "dist",
      "lib": ["esnext"],
    }
  }
````
package.jsonに下記内容を修正、追加します。
````package.json
  //修正
  "main": "server.ts"
　//追加
  "scripts": {
    "start": "nodemon server.ts",
  }
````

backend直下にserver.tsを作成します。
このファイルでAPIのサーバーの設定を行います。
````server.ts

import express, { response } from 'express';
import { BoardsController } from './controllers/boardsController';
import cors from 'cors'


require('dotenv').config();


const app: express.Express = express();
const port = 3000;

const boardsController = new BoardsController();

app.use(express.json());
app.use(cors());


app.listen(port,() => {
    console.log("起動"+ port);
})


app.get("/",(request:express.Request,response:express.Response) => {
    response.send("HTTPリクエスト確認");
})

app.get("/boards",boardsController.getAllBoards);
app.get("/boards/:id",boardsController.getBoardById);
app.post("/boards",boardsController.createBoard);
app.put("/boards/:id",boardsController.updateBoard);
app.delete("/boards/:id",boardsController.deleteBoard);

app.use((error:any,request:express.Request,response:express.Response, next:express.NextFunction) => {
    console.error(error);
    response.status(500).send("エラーが発生しました。");
});
````
backend直下にcontrollersフォルダを作成し、
boardsController.tsを作成します。
ここで、HTTPリクエストの処理を行うメソッドを定義していきます。

````boardsController.ts
import express from 'express';
import { BoardsModel } from '../models/boardsModel';

const boardsModel = new BoardsModel();


export class BoardsController {
    
    async getAllBoards(request:express.Request,response:express.Response, next:express.NextFunction)  {
        try {
            const rows = await boardsModel.getAllBoards();
            response.json(rows);
        } catch (error:any) {
            next(error);
        }
    }

    async getBoardById(request:express.Request,response:express.Response, next:express.NextFunction){
        const boardId = request.params.id;
        try {
            const rows = await boardsModel.getBoardById(boardId);
            response.json(rows);
        } catch (error:any) {
            next(error);
        }
    }

    async createBoard(request:express.Request,response:express.Response, next:express.NextFunction) {
        const {name, board_order } = request.body;
        try {
            const result  = await boardsModel.createBoard(name,board_order);
            response.json(result);
        } catch (error:any) {
            next(error);
        }
    }

    async updateBoard(request:express.Request,response:express.Response, next:express.NextFunction) {
        const {id} = request.params;
        const {name, board_order } = request.body;
        try {
            const result = await boardsModel.updateBoard(id,name,board_order);
            response.json(result);
        } catch (error:any) {
            next(error);
        }
    }

    async deleteBoard(request:express.Request,response:express.Response, next:express.NextFunction) {
        const {id} = request.params;
        try {
            const result = await boardsModel.daleteBoard(id);
            response.json(result);
        } catch (error:any) {
            next(error);
        }
    }

}
````

backend直下にmodelsフォルダを作成し、
boardsModels.tsを作成します。
ここでは、データベースにおけるCRUD処理を記述します。

````boardsModels.ts
import mysql from 'mysql2/promise';


require('dotenv').config();


const dbConnect = mysql.createPool({
    host : process.env.DB_HOST,
    user : process.env.DB_USER,
    password : process.env.DB_PASSWORD,
    database : process.env.DB_NAME,
    connectTimeout: 100,
});

export class BoardsModel {
    
    async getAllBoards() {
        const [rows] = await dbConnect.execute("select * from Boards order by board_order ");
        return rows;
    }

    async getBoardById(id:string) {
        const [rows] = await dbConnect.execute("select * from Boards where id = ? ",[id]);
        return rows;
    }

    async createBoard(name:string,board_order:number){
        const [result] = await dbConnect.execute("insert into Boards set name = ?,board_order = ?  ",[name, board_order]);
        return result;
    }

    async updateBoard(id:string,name:string,board_order:number){
        const [result] = await dbConnect.execute("update Boards set name = ?, board_order = ? where id = ?", [name, board_order, id]);
        return result;
    }

    async daleteBoard(id:string) {
        const [result] = await dbConnect.execute("delete from Boards where id = ?", [id]);
        return result;
    }


}
````
以上で、Node.jsの設定が完了です。
これで、API機能の完成です。

:::note info

この状態で、「http://localhost:3000/」へアクセスすると
app.get("/")のレスポンスで設定していたsend()の内容が返却されます。

![スクリーンショット 2023-07-11 18.14.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3495347/589c0364-db46-33a6-90ea-3d19d48d044b.png)

:::

#### 4.APIに対するHTTPリクエストの確認

APIの動作確認として、今回はchromeの拡張機能であるTalend API Testerを使用してみました。

> Talend Cloud API Tester
> https://help.talend.com/r/ja-JP/Cloud/api-tester-user-guide

まず、確認用のデータをBoardsテーブルに用意します。
![スクリーンショット 2023-07-11 19.42.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3495347/3e32eb7c-25f5-efc7-a9c0-74cd360fff83.png)

##### ①Getリクエスト
画面上部のタブから「リクエスト」を選択します。
メソッドから「GET」を選択します。
server.tsにて設定したURL「localhost」(http://localhost:3000/boards)
を指定し、送信を押下します。

![スクリーンショット 2023-07-12 9.26.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3495347/8e26fa87-2772-b324-ca07-83a3693a2a20.png)

画面下部にリクエストのレスポンス結果が表示されます。
先ほど追加した２つのデータがJSON形式で返却されることが確認できました。
![スクリーンショット 2023-07-12 9.36.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3495347/4b32eb58-c22e-66d2-4419-8a83ec5ca60a.png)


##### ②Postリクエスト
次に、POSTリクエストでデータを追加してみましょう。
メソッドから「POST」を選択します。
ボディに追加用のデータをJSON形式で記述し、送信を押下します。
![スクリーンショット 2023-07-12 9.47.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3495347/8d815c1f-f2bb-5a9a-212c-b2a9d8dba4e5.png)

下記のようなレスポンスが返却されます。
![スクリーンショット 2023-07-12 10.06.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3495347/b99c3958-3de1-1d86-4f77-914f9024f90c.png)

再び、Getリクエストを送信してデータを確認してみると、Postリクエストで送信したデータが追加されていることが確認できます。

![スクリーンショット 2023-07-12 10.09.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3495347/a18fdec0-289f-1a73-570d-93a4771e774d.png)

##### ③Putリクエスト
今度は、先ほど追加したデータを更新してみましょう。
メソッドから「PUT」を選択し、URLに更新したいデータのidを指定し、送信を押下します。

![スクリーンショット 2023-07-12 10.12.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3495347/5ea0dfc7-a1aa-601d-7e9e-420f040b6d19.png)

下記のレスポンスが返却されたので、再度Getリクエストを送信し、対象のデータが更新されたか確認してみましょう。

![スクリーンショット 2023-07-12 10.14.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3495347/2c720872-4dba-0b1a-f0df-9c24d72ecb47.png)

先ほどPutリクエストで送信した内容で更新されたことが確認できます。

![スクリーンショット 2023-07-12 10.16.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3495347/7d149dfb-29da-1965-7cf5-62af428cc132.png)

##### ④Deleteリクエスト
最後にDeleteリクエストでデータを削除してみましょう。
メソッから「DELETE」を選択し、URLに削除したいデータのidを指定し、送信を押下します。

![スクリーンショット 2023-07-12 10.20.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3495347/d90c96d4-cb29-7cfb-e3f4-2edc28b4245a.png)

![スクリーンショット 2023-07-12 10.21.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3495347/e8794420-ba27-9ebd-44e5-044a5e3f7bb4.png)

Getリクエストで削除されたかどうか確認してみましょう。
無事削除が正常に処理されていました。

![スクリーンショット 2023-07-12 10.22.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3495347/bf1de301-685a-615a-b994-82d1c30882f3.png)


以上で、実装したHTTPリクエストが全て正常に処理されていることが確認できました。
これにて、Node.js、Mysql、Dockerを使用したAPIの完成です。


## 最後に
初めてNode.jsやDockerを使用したAPI構築を行いましたが、環境構築でかなり手間取ってしまいました。
しかし、一から作り上げることで仕様を学ぶいい機会となりました。
この調子で最終目標であるtrelloのクローンアプリ完成を目指していきます。

最後まで記事を読んでいただきありがとうございました。