【備忘】Express.js + PostgreSQLをDocker上に構築2 - TypeScript版
Introduction
以前、Docker上にExpress.jsとPostgreSQLを用いてWebAPIを作成するためのベースを構築する手順をまとめました。
PV(Page View)はさっぱりですが、個人的には毎回 ここを読み直すことでスムーズな作業ができているので、備忘録としてOutputを残すことが非常に有効だと実感しました。そんなわけで同じテーマの第二弾、TypeScriptでExpress.js + PostgreSQLのWebAPIのベースを構築する手順を書き記してみようと思います。
前回との違いは大きく2点、言語を純粋なJavaScriptから型の概念を持つ上位拡張言語TypeScriptに、ORM(Object-relational mapping)をSequelizeからPrismaに、それぞれ変更しています。
平日の夜、忘れないうちにと速足で書き留めているため、いつも以上に文章が雑、かつ誤植もあるかと。。。後で読み返して必要に応じて修正します(_ _))ペコッ
前置き
環境
今回は、Windows11のWSL2上にインストールしたUbuntu 22.04.3 LTS を使用し、その上でdockerを起動・各種アプリケーションを動かします。WSL2やdocker等の基盤の環境設定手順は割愛。
ワークフォルダ
WSL2 Ubuntu上に下記構成のフォルダを用意します。
- express_on_typescript
- node
- app
- postgresql
- psdata
完成品
dockerコンテナ作成
dockerfile作成
node.js環境用Dockerfileを作成
express_on_typescript/node/ に、node.js用のコンテナの元となるDockerfileを作成します。
FROM node:lts-alpine
ENV NODE_ENV=development
WORKDIR /app
RUN apk update && \
apk add git && \
apk add openssh && \
npm install -g express-generator
ENV HOST 0.0.0.0
EXPOSE 3000
PostgreSQL環境用Dockerfileを作成
同様に、PostgreSQL用のDockerfileを、express_on_typescritp/ に作成。
FROM postgres:16.2
RUN localedef -i ja_JP -c -f UTF-8 -A /usr/share/locale/locale.alias ja_JP.UTF-8
ENV LANG ja_JP.utf8
docker-compose作成
docker-compose定義作成
node.jsとPostgreSQLのコンテナをまとめて管理するため、docker-compose定義を、express_sandbox/ に作成。サービスの名称として、node.jsコンテナを webserver、PostgreSQLコンテナを db とします。
またデータの永続化のため、nodeは、/app配下を最初に作成したホスト上の/node/appへ、PostgreSQLは、/var/lib/postgresql/data配下を/postgresql/psdata にマウントします。この設定をしておかないとコンテナを終了した際に、データが消えてしまうため。
version: '3'
services:
webserver:
build: node
tty: true
volumes:
- .:/workspace:cached
- ./node/app:/app
ports:
- "3000:3000"
- "5555:5555"
db:
build: postgresql
volumes:
- ./postgresql/psdata:/var/lib/postgresql/data
ports:
- "5432:5432"
environment:
POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --locale=ja_JP.UTF-8"
POSTGRES_PASSWORD: 'password'
あわせて、この後コンテナをVSCodeで開いた際に、ルートフォルダ(express_on_typescript)を参照できるよう、ホスト側のルート . を、コンテナ側の/workspaceにマウントしておきます。この経緯は下記参照。
docker-composeからビルド・起動
作成したコンテナ定義から、ビルドする。WSL2のターミナル上から express_on_typescritp/ にて下記コマンド実行。
docker-compose build
ビルド完了後、コンテナを起動します。
docker-compose up -d
docker-compose ps から起動中のコンテナを確認し、2つのコンテナが起動していればOK。
docker-compose ps
NAME IMAGE SERVICE STATUS
express_on_typescript-db-1 express_on_typescript-db db Up 36 seconds
express_on_typescript-webserver-1 express_on_typescript-webserver webserver Up 36 seconds
Express.jsサンプルプロジェクト作成
事前準備
WSL2上から下記コマンドを実行することで、起動中のnode.jsコンテナに入ります。以降はコンテナ上での作業となります。
docker-compose exec webserver /bin/sh
コンテナに入り込むことができたら、app直下に、nodeプロジェクトの初期化実行します。
npm init -y
プロジェクトの初期化が完了したら、続いて必要なパッケージを順に追加します。まずはepress.js関連のパッケージを追加。
npm install express cors dotenv
続いて、TypeScript開発関連のパッケージを追加。
npm install typescript @types/express @types/cors @types/dotenv ts-node nodemon --save-dev
最後に、TypeScriptの初期設定をします。下記コマンドを実行。
npx tsc --init
tsconfig.jsonという設定ファイルが作られるので、これを開き下記1文を追加します(コメントアウトされていると思うので、outDirで検索してみると見つかります)。これはTypeScriptをコンパイルした後のJavaScriptをどこに出力するかという定義になります。今回はapp配下の/distを指定します。
"outDir": "./dist",
動作確認
ここまででTypeScript版Express.js環境が構築できたので、お決まりの"Hello World!"タイムといきましょう。express_on_typescript/app配下にapp.tsを追加、下記コードを記述します。
import express, { Request, Response } from "express";
import dotenv from "dotenv";
import cors from "cors";
dotenv.config();
const app = express();
app.use(cors());
const PORT = 3000;
app.get('/', (req, res) => { res.json({title : "Hello World, express.js on TypeScript"}); });
app.listen(PORT, () => {
console.log("Server running at PORT: ", PORT);
});
TypeScriptの最も基本的な実行方法は、一度 記述したソースコードをコンパイルし、JavaScriptのファイルを作成し、そのJavaScriptファイルから実行する必要があります(この手順を簡略化する方法は後述)。
まずはTypeScript→JavaScriptのコンパイルをしましょう。下記コマンドを実行します。
npx tsc
正常終了すると、/dist配下にapp.jsが作られていると思います。こいつを実行してみましょう。
node ./dist/app.js
ポート:3000 でプログラムが起動しているはずなので、WEBブラウザから http://localhost:3000/ にアクセス、Hello World とjson形式でお返事してくれていればOK。
TypeScriptでExpress.jsを動かすことができました。でも毎回コンパイルして、jsファイルから起動するのはめんどくさいですよね。自動でコンパイルし、起動しなおすことなく自動反映してほしい。ということで少し工夫しましょう。
まずjsファイルを作成しなくてもファイルを実行できるコマンドとして ts-node があります(最初にパッケージ追加しましたね)。これを使えば、tsファイルから直セルアプリケーションを起動することができます。
続いて、変更を自動で検知し、再読み込みしてくれるコマンド、nodemon。これがあれば変更都度再起動する必要がなくなります。
この2つを組み合わせて、実行用スクリプトを作成しましょう。/app配下のpackage.jsonを開きます。ここのscript部分に下記startスクリプトを追加します。
"scripts": {
"start": "nodemon app.ts"
},
この状態でstartスクリプトを起動します。このスクリプト1発で先ほど同様アプリケーションが立ち上がるようになりました。
npm start
PostgreSQLとの接続
続いて、PostgreSQLと接続し、DBから取得した内容をもとにお返事できるようにします。前回は、ORM(Object-relational mapping)としてSequelizeを使用しましたが、今回はPrismaを使用します(SequelizeとTypeScriptの組み合わせは情報が少なく、動かせなかったため)。
Prismaの追加 & 初期設定
最初にPrismaのパッケージを追加します。
npm install prisma @prisma/client
次にtsconfig.jsonに下記1文を追加します。
"lib": ["esnext"],
ここから初期設定をしていきます。初期化コマンドを実行。
npx prisma init
.envファイルが作られ DATABASE_URL が初期設定されていると思います。ここにPostgreSQLの接続情報を記述します。形式は、
postgresql://USER:PASSWORD@HOST:PORT/DATABASE
今回、dockerの設定として
- DBユーザ:postgres
- DBユーザPW:password
- HOST:docker-composeのサービス名 db
- PORT:5432
- DB名:postgres
として定義しているので、下記のように修正します。
DATABASE_URL="postgresql://postgres:password@db:5432/postgres"
これで初期設定は完了。次に実際にデータモデルを定義、migrationを実行しDBへ定義を反映してみます。
データモデルの定義
DBへのテーブル登録と対応するプログラム側のデータモデル登録のためのマイグレーションスクリプトを生成します。
今回はお試しということで、姓・名・メールアドレスを管理するモデル:ApUser を作成してみます。
/app/prisma配下に作成された schema.prisma を開きましょう。ここにモデルの定義を追記していきます。下記記述を追加しましょう。
model ApUser {
id Int @default(autoincrement()) @id
firstName String?
lastName String?
email String?
disabled Boolean @default(false)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
}
ここからmigrationスクリプトを作成・実行するコマンドを流します。
npx prisma migrate dev --name init
正常終了するとmigrationファイルが作成されると同時に、DBにもモデルが反映されます。試しにDBの中を覗いて(私はクライアントソフトとしてA5:SQLを使用)、テーブルが作られていたらOK。
この後のテスト用に数件、適当なレコードを追加します。
ちなみに、
npx prisma studio
を実行すると、http://localhost:5555 からモデルの参照・編集が可能なブラウザアプリが起動します。ここからレコードを追加することも可能。
データ取得プログラム動作確認
最後に、Express.jsが、PostgreSQLのDBから取得した内容をもとにお返事してくれるようプログラムを改修し、Express.js + PostgreSQLサンプルプログラムの動作を確認します。
import express, { Request, Response } from "express";
import dotenv from "dotenv";
import cors from "cors";
import { PrismaClient } from "@prisma/client";
dotenv.config();
const app = express();
app.use(cors());
const PORT = 3000;
const prisma = new PrismaClient();
app.get('/', (req, res) => {
res.json({title : "Hello World, express.js on TypeScript"});
});
app.get('/users', (req, res) => {
prisma.apUser.findMany({
where:{
disabled : false
}
}).then((users)=>{
res.json(users);
})
})
app.listen(PORT, () => {
console.log("Server running at PORT: ", PORT);
});
この状態で、http://localhost:3000/users にアクセスし、DBに登録した値がレスポンスとしてかえってきていれば成功です!!
終わりに
忘れないうちに、連休の最後に試した内容を走り書きしてみました。1日前にやったことなのに、再度手順をなぞろうとすると既に忘れていること多々で、WEBブラウザの履歴をあさる羽目になりました。連休明けの平日の夜、頑張って書いてみてよかったと思えるときがいずれ来ることを祈り。
参考文献
Comments