🦧

DockerでPostgres、Node、Typescript の環境構築(サンプルコード付き)

2023/03/25に公開

はじめに

こんにちは。
full-stack developerを目指しているShenです。
DockerでPostgres、Node、Typescriptの基本な環境構築方法を共有したいと思います。
サンプルコードのrepoはこちらから取得できます。🙇‍♂️

前提条件

dockerがインストールされて、起動していること。
ちなみに、私はdocker20.10.14を使っていました。

ディレクトリ構成

BookManagement/
  ├── db
  │   └── setup.sql
  ├── dist
  │   └── app.js
  ├── src
  │   └── app.ts
  ├── docker-compose.yml
  ├── Dockerfile
  ├── node_modules
  ├── package-lock.json
  ├── package.json
  └── tsconfig.json

npmパッケージ

該当プロジェクトのruntime用パッケージをインストールする

npm install express pg uuid
  • express: Node.jsの開発フレームワーク
  • pg: Node.js用のPostgreSQLクライアント
  • uuid: ランダムのIDを発行するため

TypeScriptを使うため、@typeがないとエラーが出てくるので、それぞれの@typeも入れましょう。

npm install @types/express @types/pg @types/uuid --save-dev

開発用のパッケージもインストールする。

npm install dotenv nodemon typescript --save-dev
  • dotenv: 環境変数用
  • nodemon: ファイルの編集したことを監視して、サーバを立ち直さなくても修正を反映できる

package.json

"scripts": {
    "dev": "nodemon -L -e ts --exec \"npm run build && npm start\"",
    "start": "node ./dist/app.js",
    "build": "tsc"
 }
  • build: typescriptをビルドして、拡張子を.tsから.jsに変換する。
  • start: nodeで./dist/app.jsを実行する
  • dev:
    • -Lはファイル監視機能がサポートされていないファイルシステムが必要です。(例:一部Linuxベースのコンテナが必要です)                                Docker for MacやDocker for Windowsなどは付けなくても良いです
    • -eは監視するファイルの拡張子を指定する
    • --exec引数のコマンドを実行する

tsconfig.json

{
"compilerOptions": {
"target": "es6" /* 生成されるJavaScriptのバージョンを設定し、互換性のあるライブラリ宣言を含める*/,
"module": "commonjs" /* 生成されるモジュールコードを指定する */,
"typeRoots": ["./node_modules/@types"] /* 依存パッケージ用のtypeフォルダを指定する */,
"resolveJsonModule": true /* .jsonがインポートできるようにする */,
"outDir": "./dist" /* tscでビルドしたソースの出力先 */,
"esModuleInterop": true /* CommonJSモジュールのインポートをサポートする */
"forceConsistentCasingInFileNames": true /* インポート時に大文字小文字が正しいことを確認します。例:macは大小文字区別しないですが、linuxが区別ありなので、設定した方がソースが移植しやすくなる */,
"strict": true /* すべての厳格な型チェックオプションを有効にします。 */,
"skipLibCheck": true /* .d.tsファイルの型チェックをスキップします。 */
}

postgresの設定

まずは.envでデータベース用の必要な情報を作成する

DB_HOST='db'
DB_PORT=5432
DB_NAME='db_name'
DB_USER='postgres'
DB_PASSWORD='password'

次は、docker-compose.ymlファイルを作成する
docker-composeは複数コンテナを管理しやすくために使っています

version: '3.8'
services:
  db:
    container_name: postgres
    image: postgres
    ports:
      - 5433:${DB_PORT}
    volumes:
      - data:/data/db
      - ./db:/docker-entrypoint-initdb.d
    environment:
      - POSTGRES_PASSWORD=${DB_PASSWORD}
      - POSTGRES_DB=${DB_NAME}

volumes:
  data: {}
  • image: Docker Hubからpostgresのイメージを利用する
  • ports: ${DB_PORT}をlocalhostのport:5433にマウントする
  • volumes:
    • /data/db: コンテナをシャットダウンしてもデータがなくならないようにする
    • docker-entrypoint-initdb.d: dbの初期化sqlを実行するため
  • environment: 環境変数を設定する

さらに、初期化用の./db/setup.sqlを作成する
docker-compose.yml./db:/docker-entrypoint-initdb.dをすると、データベースを初期化する際に、setup.sqlが実行される。つまり、postgresコンテナが起動後、booksというテーブルはすでに存在している状態になる

set client_encoding = 'UTF8';

CREATE TABLE IF NOT EXISTS books (
    "id" VARCHAR(100) NOT NULL,
    "title" VARCHAR(100) NOT NULL,
    "author" VARCHAR(100) NOT NULL,
    "pages" INTEGER NOT NULL,
    PRIMARY KEY ("id")
)

最後にapp.tsからpostgresとの接続処理を追加する

import { Pool } from 'pg';

dotenv.config();// .envの環境変数をprocess.envからアクセスできるようにする

const pool = new Pool({
  host: process.env.DB_HOST,
  user: process.env.DB_USER,
  database: process.env.DB_NAME,
  password: process.env.DB_PASSWORD,
  port: parseInt(process.env.DB_PORT || '5432'),
});

const connectToDB = async () => {
  try {
    await pool.connect();
  } catch (err) {
    console.log(err);
  }
};
connectToDB();

appのサーバーをimage化にする

まずは、appのサーバーポートを.envに追加する。

PORT=3000

次は、Dockerfileを生やす。

FROM node:16.18.1

WORKDIR /usr/src/app

COPY ["package.json", "package-lock.json", "tsconfig.json", ".env", "./"]

COPY ./src ./src

RUN npm install

CMD npm run dev
  • FROM node:16.18.1: nodeのベースイメージをインストールする
  • WORKDIR /usr/src/app: appのサーバーは該当パスで実行される
  • COPY:localからファイルコンテナにコピーする
  • RUN npm install: 依存パッケージをインストールする
  • CMD npm run dev: package.jsonを参照すると、"nodemon -L -e ts --exec \"npm run build && npm start\""が実行される

Dockerfileを作成した後、appはイメージとして、コンテナで実行できるようになる。docker-compose.ymlservices配下に以下を追加する。

  api:
    container_name: api
    restart: always
    build: .
    ports:
      - ${PORT}:${PORT}
    depends_on:
      - db
    volumes:
      - .:/usr/src/app
  • restart: alwaysを指定すると、コンテナを自動起動できるようになる
  • build: Dockerfile のあるディレクトリのパスを指定する
  • ports: コンテナのport:3000localhost:3000にマウントする
  • depends_on: dbコンテナがスタート完了した後、該当コンテナを実行する

expressでエンドポイントを生やす

次はapp.tsにエンドポイントを作成する。(postgresのコードも含まれる)

import express from 'express';
import dotenv from 'dotenv';
import { v4 } from 'uuid';
import { Pool } from 'pg';

dotenv.config();

const pool = new Pool({
  host: process.env.DB_HOST,
  user: process.env.DB_USER,
  database: process.env.DB_NAME,
  password: process.env.DB_PASSWORD,
  port: parseInt(process.env.DB_PORT || '5432'),
});

const connectToDB = async () => {
  try {
    await pool.connect();
  } catch (err) {
    console.log(err);
  }
};
connectToDB();

const app = express();
app.use(express.json());

app.get('/books', async (req, res) => {
  const query = {
    text: `SELECT * FROM books`,
  };

  const records = await pool.query(query);

  res.status(200).send(records.rows);
});

app.get('/books/:bookId', async (req, res) => {
  const { bookId } = req.params;
  console.log('aabbb');
  const query = {
    text: `SELECT * FROM books WHERE id = $1`,
    values: [bookId],
  };

  const records = await pool.query(query);

  res.status(200).send(records.rows[0]);
});

app.post('/books', async (req, res) => {
  const { id, title, author, pages } = req.body;

  const bookId = id ?? v4();

  try {
    const query = {
      text: 'INSERT INTO books(id, title, author, pages) VALUES($1, $2, $3, $4)',
      values: [bookId, title, author, pages],
    };

    await pool.query(query);

    res.status(200).send({ id: bookId });
  } catch (error) {
    res.status(500).send(error);
  }
});
app.listen(process.env.PORT, () => {
  console.log(`Server is listening on port ${process.env.PORT}`);
});

起動

ターミナルでBookManagement配下で以下のコマンドを実行する。

docker-compose up

動作確認

三つのエンドポイントを生やしたので、リクエストして正しく動作できていることを確認する。

  • booksの基本情報を取得用(全件) GET /books
curl --location 'http://localhost:3000/books'

レスポンスは[]であるはず。statusは200であることは、postgresの初期化sqlが無事に実行されたことがわかる。

  • bookの基本情報を登録用(一件) POST /books
curl --location 'http://localhost:3000/books' \
--header 'Content-Type: application/json' \
--data '{
    "id": "testId",
    "title": "The Time Traveler'\''s wife",
    "author": "Audrey Niffenegger",
    "pages": 1203
}'

これでデータ1件登録完了。

  • bookの基本情報を取得用(一件) GET /books/{bookId}
curl --location 'http://localhost:3000/books/testId'

レスポンスは以下のようになれば、動作確認完了です。

{
    "id": "testId",
    "title": "The Time Traveler's wife",
    "author": "Audrey Niffenegger",
    "pages": 1203
}

終わりに

最後までご覧いただいて、ありがとうございます!
以上はDockerでPostgres、Node、Typescript の環境構築でした。
結構雑なソースですが、次回から上記のソースを使って、オニオンアーキテクチャの紹介ベースとして、リファクタリングの記事に書こうと思っています。

Discussion