TypeScript を使って、 Node.js で DB アクセスするシンプルな Web アプリケーションを作ってみようと思います。
Node.js: v14.17.0
npm: 6.14.13
TypeScript: 4.3.2
express: 4.17.1
MariaDB: 10.5.10
事前準備
Node.js インストールと package.json 作成
以下のいずれかの方法で Node.js をインストールします。
次に、package.json
を作成します。
npm init
TypeScript インストールと tsconfig.json 作成
npm を使用して、TypeScript をインストールします。
npm install --save-dev typescript @types/node@14
次に、tsconfig.json
を作成します。以下で雛形を作成して、
npx tsc --init
プロジェクトに合わせて修正します。
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"sourceMap": true,
"outDir": "./dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"]
}
色々オプションがあるので迷いますが、とりあえずは上記で進めることにしました。
コンパイルおよび実行テスト
test.ts
という TypeScript のファイルを作成し、コンパイルして、Node.js で実行してみます。
mkdir src
touch src/test.ts
echo "console.log('logging test');" > src/test.ts
npx tsc
ls dist
> test.js test.js.map
node dist/test.js
> logging test
ここもサクッと動きました。
express で Web サーバを立てて Hello World!!
まずは、express モジュールをインストールします。
npm install express
npm install --save-dev @types/express
次に、src/index.ts
を作成します。
import express from 'express';
const app = express();
app.get('/', (req: express.Request, res: express.Response) => {
res.send('Hello World!!');
})
app.listen(3000);
最後に、コンパイルして実行します。
npx tsc
node dist/index.js
curl http://localhost:3000
> Hello World!!
無事,Hello World!!
が返ってきました。
DB 構築
今回は、MariaDB という MySQL から派生したオープンソースのデータベースを利用してみます。
DockerHub 上で公開されている公式イメージ を使って、docker-compose で DBを立ち上げる方針で行きます。
まず、mariadb
コマンドで DB アクセスしたいので、Mac に MariaDB をインストールします。
brew update
brew info mariadb
brew install mariadb
mariadb --version
> mariadb Ver 15.1 Distrib 10.5.9-MariaDB, for osx10.16 (x86_64) using readline 5.1
次に、 DB のデータがdocker-compose down
で失われないように、/db_data
フォルダを作成します。
mkdir db_data
その上で、api/docker-compose.yml
を作成して、
version: '3.1'
services:
db:
image: mariadb:latest
restart: always
ports:
- "3306:3306"
volumes:
- "./db_data:/var/lib/mysql"
environment:
MARIADB_ROOT_PASSWORD: "root_password" # `root`ユーザのパスワード
MARIADB_DATABASE: "sampledb" # 初期化時に作成するデータベース
MARIADB_USER: "appuser" # 初期化時に作成するユーザ
MARIADB_PASSWORD: "appuser_password" # 初期化時に作成するユーザのパスワード
Docker Compose で MariaDB のコンテナを立ち上げます。
docker-compose up -d
mariadb
コマンドで接続してみると、無事接続できました。
mariadb -h 127.0.0.1 -P 3306 -D sampledb -u appuser -p
> Welcome to the MariaDB monitor. Commands end with ; or \g.
> Your MariaDB connection id is 4
> Server version: 10.5.10-MariaDB-1:10.5.10+maria~focal mariadb.org binary distribution
注) 設定が正しそうなのに、エラーが発生する時は、
-
db_data
に古いDB定義が残ってるERROR 1045 (28000): Access denied for user 'appuser'@'xx.xx.xx.xx' (using password: YES)
- フォルダを一旦削除してDB定義を再作成する
-
localhost
を指定してしまってるERROR 2002 (HY000): Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2)
-
127.0.0.1
を指定する
などを疑うと良いかもしれません。
最後に、テーブルも作成しておきます。
id
とcomment
の二つのカラムを持つ、comments
テーブルです。
MariaDB [sampledb]> create table comments (
id int auto_increment
, comment text not null
, primary key(id)
);
Query OK, 0 rows affected (0.011 sec)
MariaDB [sampledb]> show columns from comments;
+---------+---------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+---------+---------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| comment | text | NO | | NULL | |
+---------+---------+------+-----+---------+----------------+
2 rows in set (0.003 sec)
これで、DB の準備が完了しました。
次は、Web アプリから DB にアクセスするところをやりたいと思います。
mariadb クライアントモジュールで DB にアクセス
今回は、mariadb というクライアントモジュールを使って DB にアクセスしようと思います。
npm install --save mariadb
index.ts
を以下のように書き換えます。
import express from 'express';
import * as mariadb from 'mariadb';
// DB コネクションプールの初期化
const pool = mariadb.createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
connectionLimit: 5
});
// express 初期化
const app = express();
app.use(express.json());
app.use(express.urlencoded({
extended: true
}));
// ルーティングの定義
app.get('/comments/:id', async (req: express.Request, res: express.Response) => {
let conn: mariadb.PoolConnection | undefined;
try {
conn = await pool.getConnection();
const comment: Comment | null = await getComment(conn, Number(req.params.id));
res.send(comment);
} catch (err) {
throw err;
} finally {
if (conn) conn.release();
}
})
app.post('/comments', async (req: express.Request, res: express.Response) => {
let conn: mariadb.PoolConnection | undefined;
try {
conn = await pool.getConnection();
const id: number = await insertComment(conn, req.body.comment);
const comment: Comment | null = await getComment(conn, id);
res.send(comment);
} catch (err) {
throw err;
} finally {
if (conn) conn.release();
}
})
app.listen(3000);
// モデル定義
type Comment = {
id: number;
comment: string;
}
// DB アクセス
const getComment = async (conn: mariadb.PoolConnection, id: number): Promise<Comment | null> => {
const res: Array<Comment> = await conn.query("select * from comments where id = ?", [id]);
if (res.length === 0) return null
return res[0];
}
const insertComment = async (conn: mariadb.PoolConnection, comment: string): Promise<number> => {
const res = await conn.query("insert into comments(comment) value (?)", [comment]);
const id = res.insertId;
return id;
}
見ればわかると思いますが、ポイントは以下の通りです。
- DB コネクションプール定義で、接続定義を環境変数から取得している
- express 初期化時に、
express.json
で JSON 形式のペイロードを、express.urlencoded
で URL エンコードされたペイロードを扱えるようにする設定している - ルーティングの定義の箇所に、トランザクション境界を置いている
- DB アクセスで、SQL を発行している
以下の環境変数を設定して、コンパイル&実行します。
export DB_HOST=127.0.0.1
export DB_USER=appuser
export DB_PASSWORD=appuser_password
export DB_DATABASE=sampledb
npx tsc
node dist/index.js
curl を使って動作確認すると、以下のように問題なく動いてくれました。
curl -X POST http://localhost:3000/comments -H "Content-type: application/json" -d '{ "comment" : "new comment" }'
> {"id":51,"comment":"new comment"}
curl http://localhost:3000/comments/51
> {"id":51,"comment":"new comment"}
ts-node-dev で開発効率アップ
しばらく開発していると、毎回npx tsc && node dist/index.js
するのが面倒になってきます。
そのような場合は、ファイルが変更されたら自動でコンパイルして Node.js のプロセスを再起動してくれる、ts-node-dev を使うと便利です。
モジュールをインストールして、
npm install --save-dev ts-node-dev
package.json
のscripts
に以下を追加した上で(dev:watch
は別の名前でもOK)、
"scripts": {
"dev:watch": "ts-node-dev --respawn src/index.ts"
},
以下のようにプロセスを実行します。
npm run dev:watch
これで、 Node.js のプロセスが立ち上がり、ソースコードに変更が入れば勝手に再起動してくれるようになりました。
ちなみに、環境変数の管理は direnv を使うと便利です。
Dockerコンテナ化
ここで終わっても良かったのですが、最後にアプリをDockerコンテナ化して実行するところもやっておきます。
まずはDockerfile
を作成します。
FROM node:14.17.0
WORKDIR /usr/src/app
COPY package.json .
COPY tsconfig.json .
COPY src ./src
RUN npm install
RUN npx tsc
EXPOSE 3000
CMD ["node", "dist/index.js"]
環境変数を管理するための.env
ファイルを作成します。
host.docker.internal
で、コンテナ内からホストOS(Mac)を指すことができます。
DB_HOST=host.docker.internal
DB_USER=appuser
DB_PASSWORD=appuser_password
DB_DATABASE=sampledb
イメージを作成します。タグ名は、sample-api:latest
としておきます。
docker build . -t sample-api:latest
最後に、コンテナを起動します。
docker-compose up -d # DB起動
docker run -d -p 3000:3000 --env-file=.env sample-api:latest # API起動
これでもいいのですが、イメージ作成とDB/APIを個別起動してるのが手間なので、一度で作成+起動するようにしたいと思います。
docker-compose.yml
のservices
に以下を追加します。
services:
api:
build: .
image: sample-api:latest
ports:
- "3000:3000"
env_file:
- .env
db:
...
あとは、docker-compose
でDBとAPIをまとめて起動します。
docker-compose up -d
最後に、curl を使って動作確認すると、以下のように問題なく動いてくれました。
curl -X POST http://localhost:3000/comments -H "Content-type: application/json" -d '{ "comment" : "new comment" }'
> {"id":54,"comment":"new comment"}
curl http://localhost:3000/comments/54
> {"id":54,"comment":"new comment"}
さいごに
今回、Node.js + TypeScript + DB でアプリケーションを構築するベースラインに立てたと思います。とはいえ、実際のアプリを作る時は、認証・CSRF・O/Rマッパー・DI なども必要になってくると思うので、もう少し勉強が必要そうです。