Next.js×Rails APIモードのJSONを返すアプリをDockerで環境構築して作ってみた

# はじめに
フロントエンドをNext.js 13(TypeScript)、バックエンドをRuby on RailsのAPIモードで構成するアプリケーションをDockerで環境構築しJSONを返すアプリを作ってみました。

### 参考にしたもの
* [Next.js + Ruby on Rails(APIモード) on Docker 構築手順](https://zenn.dev/taku1115/articles/6c9fa97ab37e38#1.-docker%E3%81%AE%E6%BA%96%E5%82%99)
ありがとうございます。とても参考になりました。


* [Next.js 13 app directory で記事投稿サイトを作ってみよう](https://zenn.dev/azukiazusa/articles/next-js-app-dir-tutorial)
* [【Next.js13入門】新しくなったNext.jsをキャッチアップしてみよう](https://www.youtube.com/watch?v=dvRLrsPGqto)
Next.js 13を理解する上で参考になりました。



# ディレクトリ構成
以下の通りになります。
コマンドもしくはGUI操作でディレクトリとファイルを作成してください。
```bash:next-rails
next-rails
├── backend
│   ├── Dockerfile
│   ├── Gemfile
│   └── Gemfile.lock
├── docker-compose.yml
└── frontend
    └── Dockerfile
```

# Dockerを準備
### `docker-compose.yml`の作成
データベースはMySQLを採用しています
```next-rails/docker-compose.yml
version: '3.8'

services:
  db:
    image: mysql:8.0
    command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
    environment:
      MYSQL_ROOT_PASSWORD: password
      MYSQL_DATABASE: sample
      MYSQL_PASSWORD: password
    ports:
      - 4306:3306
    volumes:
      - mysql-db:/var/lib/mysql
  backend:
    tty: true
    depends_on:
      - db
    build:
      context: ./backend/
      dockerfile: Dockerfile
    ports:
      - 3000:3000
    volumes:
      - ./backend:/app
    command: rails server -b 0.0.0.0
  frontend:
    build:
      context: ./frontend/
      dockerfile: Dockerfile
    volumes:
      - ./frontend/app:/usr/src/app
    command: 'yarn dev'
    ports:
      - '8000:3000'
volumes:
  mysql-db:
    driver: local
```

### フロントエンド(`Next.js`)のDockerfileの作成
```Dockerfile:frontend/Dockerfile
FROM node:18
WORKDIR /usr/src/app
```

### バックエンド(`Rails`)のDockerfileの作成
```Dockerfile:backend/Dockerfile
FROM ruby:3.2

ENV LANG=C.UTF-8 \
  TZ=Asia/Tokyo

WORKDIR /app
RUN apt-get update -qq && apt-get install -y nodejs default-mysql-client
COPY Gemfile /app/Gemfile
COPY Gemfile.lock /app/Gemfile.lock
RUN bundle install

CMD ["rails", "server", "-b", "0.0.0.0"]
```

### `Gemfile`の作成
```ruby:backend/Gemfile
source 'https://rubygems.org'
gem 'rails', '~> 7.0.5'
```

### ビルドする
以上のファイルの準備ができたらビルドのコマンドを実行
```zsh
docker compose build
```

# Next.jsを作成する
`yarn create`で`frontend`ディレクトリにNext.jsを作成する
```zsh
docker compose run --rm frontend yarn create next-app .
```
#### アプリの生成が完了した後
以下コマンドでコンテナを起動
```zsh
docker compose up
```
[localhost:8000](http://localhost:8000/)にアクセスしNext.jsの画面が表示されていれば成功です。
![スクリーンショット 2023-06-25 16.58.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2860566/96d6392b-6843-31ab-4ea8-c492236f6fd0.png)

# Ruby on Rails APIモード環境の作成
`backend`ディレクトリにRails APIモードのアプリを作成する
```zsh
docker compose run --rm backend bundle exec rails new . --api -d mysql
```
:::note warn
生成途中で`Gemfile`のConfilictが生じるが`y`を入力し進める
:::

### `database.yml`の編集
Rails APIアプリケーションの作成が完了した後に`database.yml`を以下の通り編集する
```backend/config/database.yml
default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password: password  #passwordと設定
  host: db #localhostからdbに設定

development:
  <<: *default
  database: sample #app_developmentからsampleと設定

test:
  <<: *default
  database: app_test

production:
  <<: *default
  database: app_production
  username: app
  password: <%= ENV["APP_DATABASE_PASSWORD"] %>

```

### Railsアプリを起動
```zsh
docker compose up --build -d
```
[localhost:3000](http://localhost:3000/)にアクセスしてRailsの画面が表示されていれば成功です。
![スクリーンショット 2023-06-25 17.13.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2860566/1e82b21d-ca79-f4eb-fd0c-d06423c75479.png)

# JSONを返すAPIを作る
### `rails g scaffold`で必要なファイルを作成
```zsh
docker compose run --rm backend bundle exec rails g scaffold post title:string
```

### マイグレートする
POSTSテーブルが作成されます
```zsh
docker compose run --rm backend bundle exec rails db:migrate
```

### `seeds.rb`を編集
`title`の部分にダミーテキストを入力
```backend/db/seeds.rb
Post.create!(
    [
      {
        title: 'Lorem ipsum dolor sit amet.'
      },
      {
        title: 'Id velit nesciunt et adipisci vitae aut eligendi sunt eum minima totam.'
      },
      {
        title: 'Et cumque maiores eos voluptate tempore ut tempora dignissimos et soluta corrupti est nihil beatae.'
      },
      {
        title: 'Et commodi quam non dolorem quia ea cumque distinctio qui exercitationem voluptate cum iusto distinctio vel modi pariatur et doloremque officiis.'
      },
      {
        title: 'In saepe unde sed obcaecati similique quo beatae amet et Quis incidunt eos molestiae nobis et nihil tenetur?'
      },
    ]
  )
```

### テーブルにテストデータを取り込む
```zsh
docker compose run --rm backend bundle exec rails db:seed
```

[localhost:3000/posts](http://localhost:3000/posts)にアクセスし`seeds.rb`で入力したダミーテキストのJSONデータが表示されていれば成功です。

![スクリーンショット 2023-07-02 16.40.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2860566/eb74def9-2c59-627a-0fae-c8a4f02f20f9.png)

# Next.jsでAPIから取得したデータを表示する
### `development.rb`を編集

`backend/config/environments/development.rb`に`config.hosts << "backend"`を追加する
```diff_ruby
require "active_support/core_ext/integer/time"

Rails.application.configure do
・・・

+ config.hosts << "backend"
end
```

### `Page.tsx`を編集
Tailwind CSSがデフォルト設定されているので多少カスタマイズしています。
```frontend/app/app/page.tsx
import { FC } from 'react';

type Post = {
  id: number;
  title: string;
};

async function getPost() {
  const res = await fetch('http://backend:3000/posts');

  if (!res.ok) {
    throw new Error('Failed to fetch Post');
  }

  return res.json();
}

const Home: FC = async () => {
  const posts: Post[] = await getPost();

  return (
    <div className="m-4">
      <h1 className="text-4xl mb-4 underline">Post List</h1>
      {posts.map((post) => (
        <p className="mb-1" key={post.id}>
          {post.title}
        </p>
      ))}
    </div>
  );
};

export default Home;
```

# 完成
[localhost:8000](http://localhost:8000/)にアクセスして以下のように表示されていれば成功です。
![スクリーンショット 2023-07-03 0.03.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2860566/85ca4d6c-5f02-f007-9778-78cb5f45e4d2.png)