Chapter 06

サーバーサイド(Express) - 実装編

is_ryo
is_ryo
2020.10.26に更新

エントリーファイルの実装

Expressがまず初めに実行するエントリーファイルを実装します。

$ mkdir src
$ cd src
$ touch index.ts

生成したindex.tsに下記の内容をコピペします。

index.ts
import express from 'express';
import cors from 'cors';

const app = express();
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

const port = 3000;

app.get('/helloWorld', (req, res) => {
  res.status(200).send({ message: 'hello, world' });
});

app.listen(port, () => {
  console.log(`Listening at http://localhost:${port}/`);
});

ビルドして実行するためのコマンドをpackage.jsonに追加します。

package.json
...
  "scripts": {
    "start": "node ./dist/index.js", // add
    "build": "webpack --config webpack.config.js", // add
    "lint": "eslint --ext .js,.ts --ignore-path .gitignore . --fix",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
...

index.tsをビルドします。

$ npm run build

実行するとdistディレクトリ以下にindex.jsが生成されます。このファイルを実行します。

$ npm run start

そうすると http://localhost:3000 でローカル環境が立ち上がります。
ブラウザで http://localhost:3000/helloWorld を開くと {"message":"hello, world"} と返ってきます。

Routeファイルを作成する

このままではpathが増えるたびにindex.tsが大きくなっていくので、routerの部分を外出しします。

$ touch router.ts

生成したrouter.tsに下記の内容をコピペします。

router.ts
import { Router } from 'express';

export const createRouter = () => {
  const router = Router();

  router.get('/helloWorld', (req, res) => {
    res.status(200).send({ message: 'hello, world' });
  });

  return router;
};

index.tsを編集します。

index.ts
import express from 'express';
import cors from 'cors';
import { createRouter } from './router'; // add

const app = express();
app.use(cors());
app.use(express.urlencoded({ extended: true }));

const port = 3000;

// remove
// app.get('/helloWorld', (req, res) => {
//   res.status(200).send({ message: 'hello, world' });
// });

app.use('/', createRouter()); // add

app.listen(port, () => {
  console.log(`Listening at http://localhost:${port}/`);
});

これでAPIサーバが立ち上がりました!

(オプション)ホットリロード対応

nodemonというパッケージを使ってホットリロード(対象ファイルを保存すると更新する)を実装します。これをしておくとファイルを保存→ローカル環境を立ち上げ直しというめんどくさい工程をスキップすることができます。
nodemonをインストールして、設定ファイルを生成します。

$ npm i -D nodemon
$ touch nodemon.json

生成したnodemon.jsonに下記内容をコピペします。

nodemon.json
{
  "restartable": "rs",
  "watch": [
    "dist"
  ],
  "env": {
    "NODE_ENV": "development"
  }
}

package.jsonにnodemon用のコマンドを追加します。

package.json
...
  "scripts": {
    "start": "node ./dist/index.js",
    "build": "webpack --config webpack.config.js",
    "dev": "nodemon dist/index.js & webpack --config webpack.config.js -w", // add
    "lint": "eslint --ext .js,.ts --ignore-path .gitignore . --fix",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
...

devコマンドを実行するとホットリロードしてくれるローカル環境が立ち上がるようになります。

Routeの設計

今回TODOアプリを作るにあたって必要な処理を考えていきます。
基本的なCRUDがあれば良さそうです。

  • TODOの作成(Create)
  • TODOの取得(Read)
  • TODOの更新(Update)
  • TODOの削除(Delete)

それぞれにあたるpathをRouteファイルに用意していきます。
router.tsに以下の内容を追記します。

router.ts
import { Router } from 'express';

export const createRouter = () => {
  const router = Router();

  // remove
  // router.get('/helloWorld', (req, res) => {
  //   res.status(200).send({ message: 'hello, world' });
  // });
  
  // add start
  // Read
  router.get('/', (req, res) => {
    res.status(200).send({ message: 'hello, world' });
  });

  // Create
  router.put('/', (req, res) => {
    res.status(200).send({ message: 'hello, world' });
  });

  // Update
  router.post('/:taskID', (req, res) => {
    res.status(200).send({ message: 'hello, world' });
  });

  // Delete
  router.delete('/:taskID', (req, res) => {
    res.status(200).send({ message: 'hello, world' });
  });
  // add end

  return router;
};

UpdateやDeleteのPathに含まれる :taskID は、PathParameterになります。

DBアクセスClass実装

DBにアクセスするClassを実装していきます。

pg,uuid,@types/pg,@types/uuidをインストールします。
@types/hogeはhogeの型情報を持っているものです。ここで言うならpg単体では型情報を持っていないので@types/pgを使って型情報を取得します。

$ npm i pg uuid
$ npm i @types/pg @types/uuid

srcディレクトリ以下にClassとなるファイルを生成します。

$ touch dbAccessor.ts

生成したdbAccessor.tsに下記の内容をコピペします。

dbAccessor.ts
import { Pool } from 'pg';
import { v4 as uuidv4 } from 'uuid';

const pool = new Pool({
  database: 'development',
  user: 'root',
  host: '127.0.0.1',
  port: 5432,
});

export class DBAccessor {
  public get = async () => {
    const client = await pool.connect();
    try {
      const query = {
        text: 'select * from public."TodoTasks"',
      };
      const result = await client.query(query);
      return result.rows;
    } catch (err) {
      console.error(err);
      throw err;
    } finally {
      client.release();
    }
  };

  public create = async (title: string) => {
    const client = await pool.connect();
    try {
      const query = {
        text:
          'insert into public."TodoTasks" (uuid, title, "createdAt", "updatedAt") VALUES($1, $2, current_timestamp, current_timestamp)',
        values: [uuidv4(), title],
      };
      await client.query(query);
    } catch (err) {
      console.error(err);
      throw err;
    } finally {
      client.release();
    }
  };

  public update = async ({
    uuid,
    title,
    status,
  }: {
    uuid: string;
    title: string;
    status: string;
  }) => {
    console.log(uuid, title, status);
    const client = await pool.connect();
    try {
      const query = {
        text:
          'update public."TodoTasks" set title = $2, status=$3 where uuid = $1',
        values: [uuid, title, status],
      };
      await client.query(query);
    } catch (err) {
      console.error(err);
      throw err;
    } finally {
      client.release();
    }
  };

  public delete = async ({ uuid }: { uuid: string }) => {
    const client = await pool.connect();
    try {
      const query = {
        text: 'delete from public."TodoTasks" where uuid = $1',
        values: [uuid],
      };
      await client.query(query);
    } catch (err) {
      console.error(err);
      throw err;
    } finally {
      client.release();
    }
  };
}

router.tsの内容を下記のように書き換えます。

router.ts
import { Router } from 'express';
import { DBAccessor } from './dbAccessor';

const dbAccessor = new DBAccessor();

export const createRouter = () => {
  const router = Router();

  // Read
  router.get('/', async (req, res) => {
    try {
      const resBody = await dbAccessor.get();
      res.status(200).send({ message: 'get success', resBody });
    } catch (err) {
      console.error(err);
      res.status(400).send({ message: 'get failed' });
    }
  });

  // Create
  router.put('/', async (req, res) => {
    try {
      if (!req.body.title) {
        res.status(400).send({ message: 'title required' });
      }
      await dbAccessor.create(req.body.title);
      res.status(200).send({ message: 'create success' });
    } catch (err) {
      console.error(err);
      res.status(400).send({ message: 'create failed' });
    }
  });

  // Update
  router.post('/:taskID', async (req, res) => {
    try {
      if (!req.body) {
        res.status(400).send({ message: 'body required' });
      }
      await dbAccessor.update({ uuid: req.params.taskID, ...req.body });
      res.status(200).send({ message: 'update success' });
    } catch (err) {
      console.error(err);
      res.status(400).send({ message: 'update failed' });
    }
  });

  // Delete
  router.delete('/:taskID', async (req, res) => {
    try {
      if (!req.body) {
        res.status(400).send({ message: 'body required' });
      }
      await dbAccessor.delete({ uuid: req.params.taskID });
      res.status(200).send({ message: 'delete success' });
    } catch (err) {
      console.error(err);
      res.status(400).send({ message: 'delete failed' });
    }
  });

  return router;
};

ローカル環境を立ち上げてCURLで実際に試してみましょう。

// Create
$ curl -X PUT http://localhost:3000/ -H "Content-Type: application/json" -d '{"title": "test"}'

// Read
$ curl -X GET http://localhost:3000/

// Update
$ cuel -X POST http://localhost:3000/${uuid} -H "Content-Type: application/json" -d '{"title":"test", "status":"wip"}'

// Delete
$ curl -X P DELETE http://localhost:3000/${uuid}

これでサーバサイドの実装は完了です。