Closed6

docker compose でExpress.js-React.js-PostgreSQL-TypeScriptの開発環境の構築

ishiyamaishiyama

API(Express.js)の環境を作っていく。

1.apiディレクトリの作成

mkdir api && cd api

2.必要なパッケージのインストール

npm init -y
npm i express
npm i -D typescript ts-node-dev prisma @types/node @types/express

3. packages.jsonとtsconfigをとりいそぎ

開発用サーバーはts-node-devを使用する。

{
  "name": "api",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "start": "node dist/index.js",
    "build": "tsc",
    "dev": "ts-node-dev --respawn --transpile-only src/index.ts"
  },
  "dependencies": {
    "express": "^4.19.2"
  },
  "devDependencies": {
    "@types/express": "^4.17.21",
    "@types/node": "^20.14.10",
    "ts-node-dev": "^2.0.0",
    "typescript": "^5.5.3"
  }
}
{
  "compilerOptions": {
    "target": "ESNext",
    "module": "commonjs",
    "outDir": "dist",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*"]
}

4. エントリーポイント(src/indexc.ts)

import express from "express";

const app = express();

app.use("/hello", (req, res) => {
  res.send("helloworld")
});

const port = process.env.PORT || 3000;
app.listen(port, () => {
  console.log(`Server is running on port ${port}`);
});

この時点でローカル環境での開発はできる。

ishiyamaishiyama

コンテナ環境を作っていく。

開発だけならdocker-compose.ymlだけで事足りるので、Dockerfileは用意しないことにする。

1. .gitignore

dist
node_modules

2. docker-compose.yml

services:
  api:
    image: node:20
    working_dir: /app
    volumes:
      - ./api:/app
      - /app/node_modules # コンテナ内node_modulesをホストのファイルシステムから分離
    ports:
      - "3000:3000"
    command: /bin/sh -c "npm i && npm run dev"

/api配下のコンテナ環境構築は一旦完了

docker compose up -d --build

変更も反映される。
docker compose logs -f api

ishiyamaishiyama

vscodeでブレイクポイントを使う

1.launch.json(.vscode/launch.json)

{
    "version": "0.2.0",
    "configurations": [
      {
        "type": "node",
        "request": "attach",
        "name": "Attach to ts-node-dev",
        "port": 9229,
        "restart": true, // ファイル変更時、デバッガも自動的に再接続させる
        "address": "0.0.0.0",
        "localRoot": "${workspaceFolder}/api",
        "remoteRoot": "/app",
        "skipFiles": [
          "<node_internals>/**", // Node.js の内部コード(例:コアモジュール)にステップインするのを防ぐ
          "${workspaceFolder}/api/node_modules/**"
        ]
      }
    ]
  }

その他のファイル修正

$ git diff --staged
diff --git a/api/package.json b/api/package.json
index 4193a27..543cb89 100644
--- a/api/package.json
+++ b/api/package.json
@@ -5,7 +5,7 @@
   "scripts": {
     "start": "node dist/index.js",
     "build": "tsc",
-    "dev": "ts-node-dev --respawn --transpile-only src/index.ts"
+    "dev": "ts-node-dev --inspect=0.0.0.0:9229 --respawn --transpile-only src/index.ts"
   },
   "dependencies": {
     "express": "^4.19.2"
diff --git a/api/tsconfig.json b/api/tsconfig.json
index 1d08bdd..c5bfa51 100644
--- a/api/tsconfig.json
+++ b/api/tsconfig.json
@@ -6,7 +6,8 @@
     "esModuleInterop": true,
     "forceConsistentCasingInFileNames": true,
     "strict": true,
-    "skipLibCheck": true
+    "skipLibCheck": true,
+    "sourceMap": true
   },
   "include": ["src/**/*"]
 }
diff --git a/docker-compose.yml b/docker-compose.yml
index 15a293f..c3f35cf 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -7,4 +7,5 @@ services:
       - /app/node_modules # コンテナ内node_modulesをホストのファイルシステムから分離
     ports:
       - "3000:3000"
+      - "9229:9229"  # デバッグポート
     command: /bin/sh -c "npm i && npm run dev"

動作確認

ishiyamaishiyama

PostgreSQLを起動してapiと連携する。

docker-compose.yml にPostgreSQLの定義を追加

$ git diff
diff --git a/docker-compose.yml b/docker-compose.yml
index c3f35cf..15958c9 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -9,3 +9,25 @@ services:
       - "3000:3000"
       - "9229:9229"  # デバッグポート
     command: /bin/sh -c "npm i && npm run dev"
+    depends_on:
+      db:
+        condition: service_healthy # db の起動が完了するまで待つ
+
+  db:
+    image: postgres:latest
+    container_name: db
+    environment:
+      POSTGRES_USER: postgres
+      POSTGRES_PASSWORD: password
+    ports:
+      - "5432:5432"
+    healthcheck:
+      test: pg_isready -U postgres
+      interval: 5s
+      timeout: 5s
+      retries: 10
+    volumes:
+      - db_volume:/var/lib/postgresql/data # データを永続化
+
+volumes:
+  db_volume:

Primsaのインストール

DB操作はPrismaを通して行う。

cd api
npm i -D prisma && npm i @prisma/client
npx prisma init

docker-compose.ymlの定義に合わせて.envを修正

- DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"
+ DATABASE_URL="postgresql://postgres:password@db:5432?schema=public" # dbはdocker-compose.ymlで定義しているホスト名

Prismaの諸ファイルを用意(スキーマとシードとClient)

/api/prisma/scheme.prismaにスキーマを定義

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id    Int     @id @default(autoincrement())
  name  String
  email String  @unique
}

スキーマ定義に則った@prisma/clientを生成

npx prisma generate

api/prisma/seedにseedファイルを作成

import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

async function main() {
  // Seed data for the User model

  const count = await prisma.user.count();

  if (count < 1) {
    const user1 = await prisma.user.create({
      data: {
        name: "John Doe",
        email: "john.doe@example.com",
      },
    });

    console.log({ user1 });
  }
}

main()

package.jsonにseedの定義する。

     "build": "tsc",
     "dev": "ts-node-dev --inspect=0.0.0.0:9229 --respawn --transpile-only src/index.ts"
   },
+  "prisma": {
+    "seed": "ts-node-dev prisma/seed"
+  },

src/index.tsをPrismaを使用してDBからデータを取得するように変更。

 import express from "express";
+import { PrismaClient } from "@prisma/client";
 
 const app = express();
 
-app.use("/hello", (req, res) => {
-  res.send("helloworld")
+app.use("/hello", async (req, res) => {
+  const prisma = new PrismaClient();
+  const user = await prisma.user.findFirst();
+  res.send("helloworld: " + user?.name)
 });

動作確認

apiコンテナの中でprisma関連のコマンドを実行

docker compose up -d --build
docker compose exec api bash
npx prisma db push
npx prisma db seed

curlを実行

ishiyamaishiyama

React(vite)の環境を作っていく。

1.viteによるReactのセットアップ

npm create vite@latest web -- --template react-ts
cd web && npm install && npm run dev

一旦、動作確認

2. apiにcorsの設定を加える

viteのサーバーはlocalhost:5173で立ち上がり、APIリクエストはクロスオリジンになってしまうので、それを許可する設定を入れる。

docker compose exec api bash -c "npm i cors && npm i -D @types/cors"

/appi/src/index.ts

import express from "express";
import { PrismaClient } from "@prisma/client";
import cors from "cors";

const app = express();

app.use(
  cors({
    origin: ["http://localhost:5173"],
  })
);

app.use("/users", async (req, res) => {
  const prisma = new PrismaClient();
  const users = await prisma.user.findMany();
  res.json(users);
});

const port = process.env.PORT || 3000;
app.listen(port, () => {
  console.log(`Server is running on port ${port}`);
});

3. webをapiからデータ取得するように変更

/web/src/App.tsxlocalhost:3000からfetchするようなファイルに変更

import { useState, useEffect } from "react";
import "./App.css";

type User = {
  id: number;
  name: string;
  email: string;
};

function App() {
  const [users, setUsers] = useState<User[]>([]);

  useEffect(() => {
    const fetchUsers = async () => {
      const response = await fetch('http://localhost:3000/users');
      const data = await response.json();
      setUsers(data);
    };

    fetchUsers();
  }, []);

  return (
    <>
      <h1>Vite + React</h1>
      <h2>with Express.js in Docker</h2>
      <div>
        <h2>API response</h2>
        <ul>
          {users.map((user) => (
            <li key={user.id}>
              {user.name} - {user.email}
            </li>
          ))}
        </ul>
      </div>
    </>
  );
}

export default App;

動作確認

このスクラップは2ヶ月前にクローズされました