エントリーファイルの実装
Expressがまず初めに実行するエントリーファイルを実装します。
$ mkdir src
$ cd src
$ touch 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に追加します。
...
"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に下記の内容をコピペします。
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を編集します。
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に下記内容をコピペします。
{
"restartable": "rs",
"watch": [
"dist"
],
"env": {
"NODE_ENV": "development"
}
}
package.jsonにnodemon用のコマンドを追加します。
...
"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に以下の内容を追記します。
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に下記の内容をコピペします。
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の内容を下記のように書き換えます。
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}
これでサーバサイドの実装は完了です。