初めてのNext.js×Railsハンズオン

# はじめに
DMM WEBCAMP Advent Calendar 2023の1日目担当のみずたです、よろしくお願いします！🎁🎅🎄

# 今回作るもの

CRUDのRとD（ReadとDestroy）のあるアプリをNext.jsとRailsを用いてハンズオン形式で作ります。CとU（CreateとUpdate）はぜひ自分で機能追加してみてください。
※解説すると長くなる部分は解説を省略するので、不明な部分はご自身で検索等をお願いしますmm
![ezgif.com-video-to-gif.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2729380/861dff7d-e870-d54c-3cea-5465b564fc6c.gif)

ハンズオンの完成コード↓
https://github.com/mizuta61/frontend_sample
https://github.com/mizuta61/backend_sample

# 記事対象者
- フロント（Next.js)とバック（Rails)を分けた開発を経験したい方

![IMG_6560 (1).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2729380/c77c5642-5a84-4718-0eae-cc066cdd7b48.jpeg)
# バージョン
Ruby on Rails 6.1.4
node v20.9.0
Next.js 14.0.0

# なぜフロントエンドとバックエンドを分けて開発するのか
前提として、かなりざっくりですが「フロント」と「バック」の違い↓
- フロント　
    - イメージ：ユーザーが目に見える部分（文字・色とか）
    - 言語：HTML, CSS, JavaScript(TypeScript)
- バック
    - イメージ：ユーザーが目に見えない部分（データの処理とか）
    - 言語：Ruby, PHP, Go, JavaScript, etc...

話戻りまして、フロントとバックを分ける大きなメリットとして **「分業と役割分担」** があります。

大きなプロダクトになれば基本複数人での開発になります。
分けることで、開発者はそれぞれ得意な領域で専念でき、プロジェクトの進捗が速くなります。

# 全部Railsで作るじゃだめ？

Q&A形式でいきます！

Q　そもそも全部Railsで全部つくればよくない？Railsできる開発者だけ集めれば済むじゃん
A　アリだけどもベストではない

補足
実際、世に出てる有名なWebアプリでRailsだけで作られてるものはあまりない気がします（僕が知らないだけの可能性ありmm）。

Q　ベストじゃない理由は？
A　Railsのフロント機能がJavaScriptに劣るから

補足
Railsでフロント部分を書く際、最初はerb等(slim, haml含む）を使うかと思います。
erbだとUIを作るのが大変だったりデメリットが多いです（他にもあります）。なのでモダンな開発（、企業）でerbは（ほぼ）使われません。
Railsはフロント部分では使わず、バックエンド（APIモード)で使うことが多いです。

Q　じゃあフロント部分はどうするの？
A　JavaScriptのライブラリ・フレームワークを使用する！

補足
現在、フロントの言語はJavaScriptが独占状態です。ライブラリ、フレームワークはReact, Next.js, Vue.js, Nuxt.jsなどがあります。

今回はその一つのNext.jsを使用していきます！
お待たせしました、ここからハンズオン開始です！

# RailsでAPIを作成

APIとは？についてはご自身で検索お願いしますmm
（今からやる部分がAPI作ってるだな、でも一旦OKです）

### 1. APIモードでアプリ作成

任意のディレクトリで以下コマンドを実行してください。
アプリが作成できたら作業ディレクトリを移動してください。
``` terminal:terminal
$ rails _6.1.4_ new backend_sample --api # --apiでAPIモードになる
$ cd backend_sample
```

### 2. scaffoldでRouting, Controller, Model, Migrationを作成
（↑RailsのAPIモードではviewは存在しません）
``` terminal:terminal
$ rails g scaffold book title:string body:text
$ rails db:migrate
```

### 3. ダミーデータの作成

`db/seeds.rb`ファイルに以下を追加。
``` diff_ruby:seeds.rb
+ 10.times do |i|
+  Book.create(title: "ダミーtitle#{i}", body: "ダミーbody#{i}")
+ end
```
以下コマンドでデータ作成＆作成されたことを確認しましょう。
``` terminal:terminal
$ rails db:seed
$ rails s 
# 別ターミナルを開いて以下コマンドを実行
$ curl http://localhost:3000/books
# 作成したデータが出力されればOK
```

### 4. CORSの設定
この後作成するフロント側（Next.js）からRailsで作成したAPIにアクセスを許可する設定を行います。
デフォルトでは許可されていないので未設定でアクセスするとエラーになります。
`Gemfile`に以下を追加してください。
``` diff_ruby:Gemfile
+ gem 'rack-cors'
```
Gemを追加したので読み込みます。
``` terminal:terminal
$ bundle install
```
`config/initializers/cors.rb`を開いて以下のように編集。
``` diff_ruby:cors.rb
- # Rails.application.config.middleware.insert_before 0, Rack::Cors do
- #   allow do
- #     origins 'example.com'
- #
- #     resource '*',
- #       headers: :any,
- #       methods: [:get, :post, :put, :patch, :delete, :options, :head]
- #   end
- # end
+ Rails.application.config.middleware.insert_before 0, Rack::Cors do
+   allow do
+     origins 'http://localhost:3001' # ← ここ（Next.js）からのアクセスを許可
+ 
+     resource '*',
+       headers: :any,
+       methods: [:get, :post, :put, :patch, :delete, :options, :head]
+   end
+ end
```
Next.jsは3001ポートで起動させるので3001ポートからのアクセスを許可します。

これでRails側の作業は終了です！

# Next.jsでフロント側の実装！

### 1. アプリ作成

任意のディレクトリで以下コマンドを実行してください。
``` terminal:terminal
$ npx create-next-app@latest
```
実行すると設定を聞かれるので今回は以下のようにします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2729380/3decddf0-7634-a4c7-7efa-8910681e7cf2.png)
設定が終わるとアプリが作成されるので以下コマンドで移動して、起動までしましょう。
``` terminal:terminal
$ cd frontend_sample && yarn dev -p 3001
```
`http://localhost:3001/`をブラウザで開いて、以下の画面になっていればOKです！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2729380/3e4a6e3b-98a4-fda6-52d1-6800a6a0ce8a.png)
以下コマンドでデフォルトページとCSSを削除しておきます。
``` terminal:terminal
$ rm app/globals.css app/page.tsx　app/layout.tsx  
```



### 2. MUI導入
MUIはReactのUIコンポーネントライブラリです。いい感じの見た目を作るために便利なもの、みたいなイメージでOKです。
（ReactはJavaScriptのライブラリ。Next.jsはReactのフレームワーク）
[MUI公式](https://mui.com/)
有名企業も使ってますね。（上記リンクから抜粋）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2729380/52db8c77-482d-8fb0-61be-14fc005dc8a1.png)

以下コマンドで導入します。
``` terminal:terminal
$ npm install @mui/material @emotion/react @emotion/styled
$ npm install @mui/icons-material # 今回はIconも使うのでこちらも
```

### 3.一覧ページ作成
Railsで作ったデータでBook一覧表示、Book詳細をモーダル表示、Book削除ができるページを作ります。
以下コマンドで`books`フォルダを作り、その中に`page.tsx`ファイルを作ります。
Next.jsの特徴として[file-system based router](https://nextjs.org/docs/app/building-your-application/routing/defining-routes#creating-routes)があります。フォルダ・ファイルの構成からルーティングを自動生成してくれます！
Railsで言う`routes.rb`が自動になったイメージでいいと思います。（すごい）
これにより`http://localhost:300１/books`のルーティングが自動生成されます。
RailsのAPIを叩く際に使う`axios`もインストールします。
``` terminal:terminal
$ mkdir app/books && touch app/books/page.tsx
$ npm install axios
```
作成したファイルの中に以下をコピペしてください！長いですmm
コードの解説は省略するので、コードの意味を知りたい方はChatGPT等でコードを投げて解説をもらってください。
```typescript:app/books/page.tsx
"use client";

import {
  TableContainer,
  Table,
  TableHead,
  TableRow,
  TableCell,
  TableBody,
  Button,
  Typography,
  Box,
  Modal,
} from "@mui/material";
import VisibilityIcon from "@mui/icons-material/Visibility";
import DeleteForeverIcon from "@mui/icons-material/DeleteForever";

import axios from "axios";
import { useEffect, useState } from "react";

type Book = {
  id: number;
  title: string;
  body: string;
  created_at: string;
  updated_at: string;
};

const BookIndex = () => {
  const [books, setBooks] = useState<Book[]>([]);
  const [selectedBookId, setSelectedBookId] = useState<number | null>(null);

  useEffect(() => {
    fetch("http://localhost:3000/books")  // Book全件取得のRailsのAPIを叩いている
      .then((res) => res.json())
      .then((books) => setBooks(books));
  }, []);

  const selectedBook = books.find((book) => book.id === selectedBookId);

  const handleShowDetails = (id?: number) => setSelectedBookId(id || null);

  const deleteBook = async (id: number) => {
    await axios.delete(`http://localhost:3000/books/${id}`); // 指定したBookを削除するRailsのAPIを叩いている
    setBooks(books.filter((book) => book.id !== id));
  };

  return (
    <>
      <Typography variant="h4" align="center">
        Book List
      </Typography>
      <TableContainer>
        <Table sx={{ maxWidth: 650 }} align="center">
          <TableHead>
            <TableRow>
              <TableCell>Title</TableCell>
              <TableCell>Body</TableCell>
              <TableCell colSpan={2}></TableCell>
            </TableRow>
          </TableHead>
          <TableBody>
            {books.map((book) => {
              return (
                <TableRow key={book.id}>
                  <TableCell>{book.title}</TableCell>
                  <TableCell>{book.body}</TableCell>
                  <TableCell>
                    <Button
                      variant="contained"
                      color="primary"
                      size="small"
                      startIcon={<VisibilityIcon />}
                      onClick={() => handleShowDetails(book.id)}
                    >
                      SHOW
                    </Button>
                  </TableCell>
                  <TableCell>
                    <Button
                      variant="contained"
                      color="error"
                      size="small"
                      startIcon={<DeleteForeverIcon />}
                      onClick={() => deleteBook(book.id)}
                    >
                      DESTROY
                    </Button>
                  </TableCell>
                </TableRow>
              );
            })}
          </TableBody>
        </Table>
      </TableContainer>

      {selectedBook && (
        <Modal open>
          <Box
            sx={{
              position: "absolute" as "absolute",
              top: "50%",
              left: "50%",
              transform: "translate(-50%, -50%)",
              width: 400,
              bgcolor: "lightblue",
              p: 4,
              borderRadius: "0.5em",
            }}
          >
            <Box component="p">ID: {selectedBook.id}</Box>
            <Box component="p">Title: {selectedBook.title}</Box>
            <Box component="p">Body: {selectedBook.body}</Box>
            <Box component="p">CreatedAt: {selectedBook.created_at}</Box>
            <Box component="p">UpdatedAt: {selectedBook.updated_at}</Box>
            <Button onClick={() => handleShowDetails()} variant="contained">
              Close ✖️
            </Button>
          </Box>
        </Modal>
      )}
    </>
  );
};

export default BookIndex;

```
では、`http://localhost:3001/books`を開いてみてください。以下の画面になればOKです。
※Railsが起動していない場合は`rails s`をしてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2729380/52b183d5-e6c4-9c6e-151a-2ab68c6d2a7a.png)
「SHOW」を押すとBookの詳細表示のモーダルが表示され、「DESTROY」を押すとBookが削除されます。

### RailsAPIが使われるタイミング
- `http://localhost:3001/books`を開いた時
    - Railsのbooks_contollerのindexアクションが実行され、 Next.js側に`@books(=Book.all)`が渡される。
- 「DESTROY」を押した時
    - Railsのbooks_contollerのdestroyアクションが実行され、指定のBookが削除されます。

# 実装終了🌟
これでフロント側とバック側の実装ともに終了です！

# まとめ
今回のようなフロントとバックを別々のアプリで作る設計はよく使われています。
Webエンジニアを目指す方は押さえておきたい技術ですね！
記事が少しでも良いと思ってもらえたら、（おそらく）左上にあるハートを押していいねをお願いします！！
お疲れ様でしたー！