Open11

Next.js / Express / Prisma / Docker Compose でプロダクトを構築するフロー

tbabatbaba

事前準備

Next.js を使うので、 create-next-app を入れておく。

$ npm install -g create-next-app@latest
tbabatbaba

構成決め

次に、ディレクトリ構成を決める。今回はモノレポでやろうと思うので、以下のような形を取る。[1]

.
├── docker-compose.yml
├── packages
│   ├── frontend
│   │   └── // Next.js
│   ├── backend
│   │   └── // Express + Prisma
│   └── infra
│       └── // AWS CDK
脚注
  1. 本スクラップでは infra ディレクトリの AWS CDK については対応しない。 ↩︎

tbabatbaba

フロントエンドの環境構築

今回は Next.js をインストールしていく。

$ cd packages
$ npx create-next-app frontend
✔ Would you like to use TypeScript with this project? … No / Yes
✔ Would you like to use ESLint with this project? … No / Yes
✔ Would you like to use Tailwind CSS with this project? … No / Yes
✔ Would you like to use `src/` directory with this project? … No / Yes
✔ Use App Router (recommended)? … No / Yes
✔ Would you like to customize the default import alias? … No / Yes
Creating a new Next.js app in $HOME/Project/hogehoge/packages/frontend.

Using npm.

Initializing project with template: app-tw 


Installing dependencies:
- react
- react-dom
- next
- typescript
- @types/react
- @types/node
- @types/react-dom
- tailwindcss
- postcss
- autoprefixer
- eslint
- eslint-config-next


added 352 packages, and audited 353 packages in 17s

136 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
Success! Created frontend at $HOME/Project/hogehoge/packages/frontend
tbabatbaba

データベース環境の構築

次にバックエンドを構築していく。その前に、バックエンドで呼び出すデータベースを構築する。
プロジェクトルートにおいてある docker-compose.yml を編集する。

今回のプロダクトは AWS での運用を想定しているので、イメージに AWS のパブリックイメージを使う。

docker-compose.yml
version: '3.9'

services:
    db:
    container_name: db
    image: public.ecr.aws/ubuntu/mysql:latest
    ports:
      - "3306:3306"
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_USER: dev
      MYSQL_PASSWORD: password
    restart: always
    volumes:
      - db:/var/lib/mysql
    networks:
      - db-network

networks:
  db-network:
    driver: bridge

volumes:
  db:
tbabatbaba

Prisma2 の導入

データベースを作ったら次はそれに接続するための環境を構築していく。今回は Prisma2 を利用することにする。

$ cd packages/backend
$ npx prisma init

✔ Your Prisma schema was created at prisma/schema.prisma
  You can now open it in your favorite editor.

Next steps:
1. Set the DATABASE_URL in the .env file to point to your existing database. If your database has no tables yet, read https://pris.ly/d/getting-started
2. Set the provider of the datasource block in schema.prisma to match your database: postgresql, mysql, sqlite, sqlserver, mongodb or cockroachdb.
3. Run prisma db pull to turn your database schema into a Prisma schema.
4. Run prisma generate to generate the Prisma Client. You can then start querying your database.

More information in our documentation:
https://pris.ly/d/getting-started

そのあとは、 Next steps に書いてある通りに進める。
まずは .envpackages/backend の中に作ってあるので、そこの DATABASE_URL を変更する。
デフォルトでは PostgreSQL で書いてあるので、 MySQL のケースに書き直す。
ここを参考にすると良いかもしれない。

packages/backend/.env
DATABASE_URL="mysql://{ユーザー名}:{パスワード}@localhost:{ポート番号}/{データベース名}"

ここまでで「接続するだけなら」できるようになっているので、接続できるかを確認するために簡単なマイグレーションを書いてみる。

packages/backend/prisma/schema.prisma
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

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

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

model Todo {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  title     String
  completed Boolean  @default(false)
}

今回は Todo というモデルを作って、テーブルを作成する。データベースにテーブル作成するために、以下のコマンドを使う。

$ npx prisma migrate dev --name init
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": MySQL database "db" at "127.0.0.1:3306"

MySQL database db created at 127.0.0.1:3306

Applying migration `20230606072246_init`

The following migration(s) have been created and applied from new schema changes:

migrations/
  └─ 20230606072246_init/
    └─ migration.sql

Your database is now in sync with your schema.

✔ Generated Prisma Client (4.14.1 | library) to ./../../../../node_modules/@prisma/client in 34ms

init という名前でマイグレーションを作成している。
これによって、 packages/backend/prisma の中に migrations というディレクトリが作成される。中身はこんな感じ。

packages/backend/prisma/migrations/yyyymmddHHMMSS_init/migration.sql
-- CreateTable
CREATE TABLE `Todo` (
    `id` INTEGER NOT NULL AUTO_INCREMENT,
    `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
    `updatedAt` DATETIME(3) NOT NULL,
    `title` VARCHAR(191) NOT NULL,
    `completed` BOOLEAN NOT NULL DEFAULT false,

    PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

これで、データベースに Todo テーブルが作成された。

tbabatbaba

シードデータを登録する

開発環境なので、シードデータが欲しい。
ここを参考にして作っていく。

まずは TypeScript と ts-node をインストールする。これは、シードデータのファイルを TypeScript で作るため、トランスパイルして実行してくれる環境は必要になるから。
同時に、クライアントになるライブラリもインストールしておく。

$ npm init -y
Wrote to $HOME/Project/hogehoge/packages/backend/package.json:

{
  "name": "backend",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

$ npm install -D typescript ts-node @types/node

added 19 packages, and audited 20 packages in 799ms

found 0 vulnerabilities

$ npm install -D prisma

added 2 packages, and audited 22 packages in 3s

found 0 vulnerabilities

$ npm install @prisma/client

added 2 packages, and audited 24 packages in 3s

found 0 vulnerabilities

そして、シードデータを保存するためのコードを書いていく。

packages/backend/prisma/seeds.ts
import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

async function main() {
  const task1 = await prisma.todo.upsert({
    where: { id: 1 },
    update: {},
    create: {
      id: 1,
      title: "Task 1",
    },
  });
  const task2 = await prisma.todo.upsert({
    where: { id: 2 },
    update: {},
    create: {
      id: 2,
      title: "Task 2",
    },
  });

  console.log({ task1, task2 });
}

main()
  .then(async () => {
    await prisma.$disconnect();
  })
  .catch(async (e) => {
    console.error(e);
    await prisma.$disconnect();
    process.exit(1);
  });

prisma.todo は、 schema.prisma で定義した Todo が適用される。そして upsert では、 where 句の条件でもし見つかった場合にはアップデートが走り、見つからなかった場合はインサートが走るという書き方ができる。

これを実行するために、必要なことを package.json に書き込んでいく。

packages/backend/package.json
{
  // ...
  "prisma": {
    "seed": "ts-node --compiler-options {\"module\":\"commonjs\"} ./prisma/seed.ts"
  },
  // ...
}

ここで npx prisma db seed とすると、実はまだ足りなくて、 @prisma/client が初期化されていない、というエラーに遭遇する。ので、実行しておく。

$ npx prisma generate
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma

✔ Generated Prisma Client (4.15.0 | library) to ./node_modules/@prisma/client in 54ms
You can now start using Prisma Client in your code. Reference: https://pris.ly/d/client
\`\`\`
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
\`\`\`

これでようやくシードデータを流し込む準備が整ったので、実行してみる。

$ npx prisma db seed
Environment variables loaded from .env
Running seed command `ts-node --compiler-options {"module":"commonjs"} ./prisma/seeds.ts` ...
{
  task1: {
    id: 1,
    createdAt: 2023-06-06T07:53:11.337Z,
    updatedAt: 2023-06-06T07:53:11.337Z,
    title: 'Task 1',
    completed: false
  },
  task2: {
    id: 2,
    createdAt: 2023-06-06T07:53:11.358Z,
    updatedAt: 2023-06-06T07:53:11.358Z,
    title: 'Task 2',
    completed: false
  }
}

🌱  The seed command has been executed.

成功。

tbabatbaba

Prisma Studio の導入

実際に保存できているかを確認するために、 Prisma Studio を導入する。
これは Prisma 経由でデータをGUIで確認できるツールで、公式から提供されている。

とは言っても難しいことは一切なく、単に以下のコマンドを実行するだけで良い。

$ npx prisma studio

こうするだけで、ブラウザで以下のような画面が確認できるようになる。

スクリーンショット

デフォルトでは URL は http://localhost:5555 となるが、ポート番号はオプションで差し替えることも可能。詳細はこちら

tbabatbaba

Express で Hello World する

データベースにアクセスできるようになったので、今度は Express を使って API サーバーを構築していく。
まずは Express 自体のインストールを行う。

$ npm install express 

added 58 packages, and audited 82 packages in 897ms

8 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
$ npm install -D @types/express

added 9 packages, and audited 91 packages in 2s

8 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

これで準備が完了したので、実装を進める。
まずは開発用なので、 Hello World から作っていく。

packages/backend/src/index.ts
import express from 'express';

const app = express();
const PORT = 3000;

app.get('/', (req, res) => {
  res.send('Hello World!');
});

app.listen(PORT, () => {
  console.log(`Running the app on port: ${PORT}`);
});

そして、これを実行するためのコマンドを作っていく。

packages/backend/package.json
{
  // ...
  "scripts": {
    "dev": "ts-node ./src/index.ts"
  },
  // ...
}

最後に、実行してみる。

$ npm run dev

> backend@1.0.0 dev
> ts-node ./src/index.ts

Running the app on port: 3000

するとこういう感じで、 http://localhost:3000 にアクセスできるようになる。

tbabatbaba

Express で API サーバーを構築する

Hello できたので、次は Prisma でデータを取得できるようにする。先ほどのファイルに、以下のように追加していく。

packages/backend/src/index.ts
import express from 'express';
import { PrismaClient } from '@prisma/client';

const app = express();
const prisma = new PrismaClient();
const PORT = 3000;

app.get('/', (req, res) => {
  res.send('Hello World!');
});

app.get('/api/todos', async (req, res) => {
  const result = await prisma.todo.findMany();

  res.json(result);
});

app.listen(PORT, () => {
  console.log(`Running the app on port: ${PORT}`);
});

これで npm run dev を再起動して、ブラウザから http://localhost:3000/api/todos にアクセスすると、データベースに保存してある Todo を JSON 形式で取得できる。

tbabatbaba

Express をホットリロードする

このままだと、何か更新するたびにコマンドを打ち直す必要があって、全く実用的ではない。なので、ファイルの変更を監視してホットリロードする仕組みを入れていく。[1]

まずは nodemon をインストールする。

$ npm install -D nodemon

そして、設定ファイルを作っていく。今回は「 src ディレクトリ内に存在するファイルを監視する」「 ts 拡張子のファイルを監視する」という条件を設定する。

packages/backend/nodemon.json
{
  "watch": ["./src"],
  "ext": "ts",
  "exec": "npm run dev",
}

それに合わせて、 package.json の scripts も変更していく。

packages/backend/package.json
{
  // ...
  "scripts": {
    "watch": "npx nodemon",
    "dev": "ts-node src/index.ts",
  },
  // ...
}

"exec" というオプションで、これまで実行していた npm run dev を動かして、通常は npm run watch を実行することで、ファイル監視を行いながらホットリロードを実現する。
この状態でファイルを変更すると、以下のようなコンソールログが表示される。

$ npm run watch

> backend@1.0.0 watch
> npx nodemon

[nodemon] 2.0.22
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): src/**/*
[nodemon] watching extensions: ts
[nodemon] starting `npm run dev`

> backend@1.0.0 dev
> ts-node src/index.ts

Running the app on port: 3000
// <- ここでファイルを変更
[nodemon] restarting due to changes...
[nodemon] starting `npm run dev`

> backend@1.0.0 dev
> ts-node src/index.ts

Running the app on port: 3000
脚注
  1. このあたりの仕組みは、今後本番リリース時の可用性なども考えたら、変えた方が良い。例えば esbuild や webpack などのバンドラーを利用して、より小さくできるようにしておいた方が良さそう。その方がホットリロードも確実なはず。 ↩︎

tbabatbaba

フロントエンドから API サーバーに問い合わせて情報を手に入れる

ようやく、フロントエンドとバックエンドの結合を行なっていく。
まずは Todo を取得するためのセグメントを作成する[1]

脚注
  1. 今回は App Router を使って Server Component を実装する ↩︎