「Nuxt + Laravelでとりあえず動くものをサクッと作る」をトレースしてみる

# はじめに

Laravelメインで仕事していましたが、最近はめっきりEC-Cube使いになってしまい、Laravel忘れそうな上野です。友人から[Zennの記事「Nuxt + Laravelでとりあえず動くものをサクッと作る」](https://zenn.dev/tak001/articles/2350c1e45edb02)をやってみたところ、エラーで動かなかったと聞いたので自分でも試そうと思い記事を書いています。

# 環境

環境は参考にしているサイトのものから、現状の最新のものにアップデートしています。またDBは環境を汚したくないのでDocker(docker-compose)にてインストールする事としました。

* MacBook Pro (Intel Core 2019モデル)
    * macOS Monterery 12.2.1
    * VSCode 
* フロントエンド
    * Node.js : 16.14.0
    * npm : 8.5.1
    * yarn : 1.22.17 （使わないけど念のため）
    * Nuxt.js : 2.15.8
    * TypeScript : 4.5.5
* バックエンド
    * PHP : 8.1.0
    * Compsoer : 2.2.6
    * Laravel : 9.0.1
* DB
    * MySQL : 8.0.28-debian (from Docker Hub with docker-compose)

# フォルダ構成

```
sample
├── docker
│   └── docker-compose.yml
├── laravel-sample
└── nuxt-sample
```

# Nuxt（フロントエンド）の構築

## 1. create (npx create-nuxt-app)

まずは参考サイトと同じく`npx create-nuxt-app`でプロジェクトを作成します。yarn好きな人はyarnでインストールしてください（[参考](https://nuxtjs.org/ja/docs/get-started/installation)）。

```text:terminal
mkdir sample
cd sample
npx create-nuxt-app nuxt-sample
```

```text:対話式の質問と答え
? Project name: nuxt-sample
? Programming language: TypeScript
? Package manager: Npm
? UI framework: None
? Nuxt.js modules: Axios - Promise based HTTP client
? Linting tools: ESLint, Prettier
? Testing framework: Jest
? Rendering mode: Single Page App
? Deployment target: Static (Static/Jamstack hosting)
? Development tools: jsconfig.json (Recommended for VS Code if you're not usi
ng typescript)
? Continuous integration: None
? Version control system: Git
```

以下のようなメッセージがでれば成功

```text
🎉  Successfully created project nuxt-sample

  To get started:

	cd nuxt-sample
	npm run dev

  To build & start for production:

	cd nuxt-sample
	npm run build
	npm run start

  To test:

	cd nuxt-sample
	npm run test


  For TypeScript users.

  See : https://typescript.nuxtjs.org/cookbook/components/
```

Nuxtのテスト起動をしてみます。

```text:terminal
cd nuxt-sample
npm run dev
```

成功すると以下のように表示される。

```text
   ╭───────────────────────────────────────╮
   │                                       │
   │   Nuxt @ v2.15.8                      │
   │                                       │
   │   ▸ Environment: development          │
   │   ▸ Rendering:   client-side          │
   │   ▸ Target:      static               │
   │                                       │
   │   Listening: http://localhost:3000/   │
   │                                       │
   ╰───────────────────────────────────────╯

ℹ Preparing project for development
ℹ Initial build may take a while
ℹ Discovered Components: .nuxt/components/readme.md
✔ Builder initialized
✔ Nuxt files generated

✔ Client
  Compiled successfully in 3.77s

ℹ Waiting for file changes
ℹ Memory usage: 266 MB (RSS: 402 MB)
ℹ Listening on: http://localhost:3000/
No issues found.
```

`Listening on: http://localhost:3000/` と表示されているのでブラウザで[http://localhost:3000/](http://localhost:3000/)にアクセスするとNuxtJSが表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65508/081d8752-2f3b-c8ca-bd41-ccfff8381a7b.png)

`Ctrl-C`でNuxtを終了しておきます。

## 2. 開発準備

普段私はPhpStormやWebStormを使っているのですが、元記事の筆者さんはVSCode使いのようなので、今回はVSCodeを使ってみます。

### VSCodeの設定

お作法が良く分かっていないので、参考サイトをそのまま写経します。

```text:terminal
mkdir ~/.vscode
vi ~/.vscode/settings.json
```

```json:~/.vscode/settings.json
{
  "[vue]": {
    "editor.defaultFormatter": "octref.vetur"
  },
  "[typescript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[javascript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "vetur.experimental.templateInterpolationService": true,
  "vetur.format.defaultFormatter.html": "prettier",
  "files.autoSave": "afterDelay",
  "eslint.alwaysShowStatus": true,
  "editor.tabSize": 2,
  "editor.formatOnPaste": true,
  "editor.formatOnType": true,
  "editor.formatOnSave": true,
  "files.insertFinalNewline": true,
  "files.eol": "\n"
}
```

### ESLintの設定

Nuxtプロジェクトルートにある`.eslintrc.js`を以下のように修正します。

```javascript:.eslintrc.js
module.exports = {
  root: true,
  env: {
    browser: true,
    node: true,
  },
  extends: [
    '@nuxtjs/eslint-config-typescript',
    'plugin:nuxt/recommended',
    'prettier',
  ],
  plugins: [],
  // add your custom rules here
  rules: {
    'no-useless-constructor': 'off',
    'vue/singleline-html-element-content-newline': 'off',
    'no-use-before-define': 'off',
    quotes: [
      'error',
      'single',
      {
        avoidEscape: true,
        allowTemplateLiterals: false,
      },
    ],
  },
}
```

## 3. proxy と nuxt.config.js の設定

今回は`proxy`を利用してCORS対策を行っているようです。なのでNuxt.js向けパッケージの`@nuxtjs/proxy`をインストールして、設定をします。

```text:terminal
npm i @nuxtjs/proxy
```

インストールが終わったら nuxt.config.js を以下のように変更します。

:::note warn
警告
元サイトにも書かれていますが、proxy URLが直書きされています。プロダクション環境では環境変数を用意しましょう。
:::

```javascript:nuxt.config.js
export default {
  // Disable server-side rendering: https://go.nuxtjs.dev/ssr-mode
  ssr: false,

  // Target: https://go.nuxtjs.dev/config-target
  target: 'static',

  // Global page headers: https://go.nuxtjs.dev/config-head
  head: {
    title: 'nuxt-sample',
    htmlAttrs: {
      lang: 'en',
    },
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      { hid: 'description', name: 'description', content: '' },
      { name: 'format-detection', content: 'telephone=no' },
    ],
    link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }],
  },

  // Global CSS: https://go.nuxtjs.dev/config-css
  css: [],

  // Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins
  plugins: [],

  // Auto import components: https://go.nuxtjs.dev/config-components
  components: true,

  // Modules for dev and build (recommended): https://go.nuxtjs.dev/config-modules
  buildModules: [
    // https://go.nuxtjs.dev/typescript
    '@nuxt/typescript-build',
  ],

  // Modules: https://go.nuxtjs.dev/config-modules
  modules: [
    // https://go.nuxtjs.dev/axios
    '@nuxtjs/axios',

    // https://www.npmjs.com/package/@nuxtjs/proxy
    '@nuxtjs/proxy', // <= 追加
  ],

  // Axios module configuration: https://go.nuxtjs.dev/config-axios
  axios: {
    proxy: true, // <= 修正
  },

  proxy: { // <= 追加
    '/api': 'http://localhost:8000', // <= 追加
  }, // <= 追加

  // Build Configuration: https://go.nuxtjs.dev/config-build
  build: {},
}
```

## 4. HTTPClient

axiosを使います。

### 外部通信メソッド作成

`BookService`を実装します。元サイトのコードのママでは動作しなかったので、一部修正してあります。

```text:terminal
mkdir service
touch service/book.ts
```

```typescript:service/book.ts
import axios, { AxiosResponse } from 'axios'

export interface BookResponse {
  id: number
  title: string
  author: string
}

export interface BookRequest {
  title: string
  author: string
}

export class BookService {
  static async fetchBooks(): Promise<BookResponse[]> {
    const { data } = await axios.get<AxiosResponse<BookResponse[]>>(
      '/api/books'
    )
    return data.data
  }

  static async postBookData(bookRequest: BookRequest) {
    await axios.post('/api/books', bookRequest)
  }

  static async fetchBook(bookId: number) {
    const { data } = await axios.get<AxiosResponse<BookResponse>>(
      `/api/books/${bookId}`
    )
    return data.data
  }

  static async putBook(bookId: number, data: BookRequest) {
    axios.put(`/api/books/${bookId}`, data)
  }

  static async deleteBook(bookId: number) {
    await axios.delete(`/api/books/${bookId}`)
  }
}
```

## 5. vueファイルの作成

`pages/index.vue`を修正します。元サイトに載っていたママでは動作しなかったので、一部修正してあります。

```vue:pages/index.vue
<template>
  <div>
    <h2>List</h2>
    <ul v-for="(book, i) in books" :key="i">
      <li>{{ book.title }}</li>
      <nuxt-link :to="{ name: 'book-detail-id', params: { id: book.id } }"
        ><button>詳細</button>
      </nuxt-link>
      <button @click="onClickDelete(book.id)">削除</button>
    </ul>
    <h3>新規追加</h3>
    <input v-model="form.title" type="text" placeholder="title" /><br />
    <input v-model="form.author" type="text" placeholder="author" /><br />
    <button @click="onClickAdd">追加</button>
  </div>
</template>

<script lang="ts">
import Vue from 'vue'
import {BookService, BookResponse } from '@/service/book'

interface Form {
  title: string
  author: string
}

type Book = BookResponse

interface DataType {
  form: Form
  books: Book[]
}

export default Vue.extend({
  async asyncData() {
    const books = await BookService.fetchBooks()
    return {
      books,
    }
  },
  data(): DataType {
    return {
      form: { title: '', author: '' },
      books: [],
    }
  },
  methods: {
    async onClickAdd() {
      await BookService.postBookData({ ...this.form })
      this.books = await BookService.fetchBooks()
      this.form = { title: '', author: '' }
    },
    async onClickDelete(bookId: number) {
      await BookService.deleteBook(bookId)
      this.books = await BookService.fetchBooks()
    },
  },
})
</script>
```

詳細ページを作ります。このファイルも元サイトのママでは動かなかったので修正しています。

```terminal
mkdir -p pages/book/detail
touch pages/book/detail/_id.vue
```

```vue:pages/book/detail/_id.vue
<template>
  <div>
    <h2>詳細</h2>
    <input v-model="book.title" type="text" />
    <input v-model="book.author" type="text" />
    <button @click="onClickEdit">修正</button>
    <nuxt-link :to="{ name: 'index' }"><p>Book List</p></nuxt-link>
  </div>
</template>

<script lang="ts">
import Vue from 'vue'
import { BookService, BookResponse } from '@/service/book'

type Book = BookResponse

interface DataType {
  book: Book
}

export default Vue.extend({
  async asyncData({ route }) {
    const bookId = Number(route.params.id)
    const book = await BookService.fetchBook(bookId)
    return {
      book,
    }
  },
  data(): DataType {
    return {
      book: {
        id: 0,
        title: '',
        author: '',
      },
    }
  },
  methods: {
     onClickEdit() {
      const bookId = Number(this.$route.params.id)
      BookService.putBook(bookId, this.book)
      this.$router.push({ name: 'index' })
    }
  }
})
</script>
```

# Laravel（バックエンド）の構築

## 0. DBの設定

DockerでサクッとMySQL8環境を作ってしまいます。XAMP, MAMPを使われている方などはスキップしてください。

```yaml:docker-compose.yml
version: '3'

services:
  db:
    image: mysql:8.0.28-debian
    command: mysqld --default-authentication-plugin=mysql_native_password --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
    environment:
      MYSQL_ROOT_PASSWORD: password
      MYSQL_DATABASE: laravel
      MYSQL_USER: laravel
      MYSQL_PASSWORD: password
    expose:
      - 3306
    ports:
      - 3306:3306
    volumes:
      - ./data:/var/lib/mysql
```

起動
```terminal
docker-compose up
```

## 1. create （composerによる）

元記事ではLaravelインストーラーを使っていますが、ここではcomposerでインストールすることにします。

```terminal
cd sample
composer create-project laravel/laravel laravel-sample
```

## 2. LocalにDBを作る

DockerにMySQLを構築しているので、既にDBは作成済みです。

```text
ROOT PASSWORD : password
DATABASE NAME : laravel
USER NAME     : laravel
USER PASSWORD : password
```

念のため作成されたDATABASE(laravel)があるか確認してみます。

```terminal
mysql -h127.0.0.1 -uroot -ppassword
mysql> SHOW DATABASES;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| laravel            |
| mysql              |
| performance_schema |
| sys                |
+--------------------+
5 rows in set (0.01 sec)
```

## 3. 各種設定

### .env

.envの`DB_`で始まる列がDatabase設定になります。見てみると以下のようになっているはずです。

```text:.env（抜粋）
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=
```

ここの`DB_PASSWORD`を修正します。

```text
DB_PASSWORD=password
```
### config/app.php

`timezone`と`locale`の修正です。

今回は72行目と85行目を修正しました（Laravelのバージョンによって変わります）

```php:config.app.php
'timezone' => 'Asia/Tokyo',
'locale' => 'ja',
```

### config/database.php

`charset`と`collation`（元サイトでは`callation`表記、TYPOか？）の変更をするよう指定がありますが、今回utf8mb4でデータベースを作成しているので変更はしません。

## 4. modelの作成

`artisan`コマンドでモデルを作成します。

```terminal
php artisan make:model Book --migration
```

以下のように`Book.php`を修正します。

```php:app/Models/Book.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Book extends Model
{
    use HasFactory;

    protected $fillable = [
        'title',
        'author',
    ];
}
```

マイグレートファイルを修正します。私の環境では`database/migrations/2022_02_19_164808_create_books_table.php`というファイルで作成されています。元サイトには`$table->bigIncrements('id')`を使うように記述されていますが、ちょっと古いので`$table->id()`を使っています。

```php:database/migrations/2022_02_19_164808_create_books_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('books', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->string('author');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('books');
    }
};
```

## 5. migrate

migrationをしていきます。

```terminal
php artisan migrate
```

確認します。

```terminal
mysql -h127.0.0.1 -uroot -ppassword laravel
mysql> SHOW TABLES;
+------------------------+
| Tables_in_laravel      |
+------------------------+
| books                  |
| failed_jobs            |
| migrations             |
| password_resets        |
| personal_access_tokens |
| users                  |
+------------------------+
6 rows in set (0.00 sec)

mysql> DESC books;
+------------+-----------------+------+-----+---------+----------------+
| Field      | Type            | Null | Key | Default | Extra          |
+------------+-----------------+------+-----+---------+----------------+
| id         | bigint unsigned | NO   | PRI | NULL    | auto_increment |
| title      | varchar(255)    | NO   |     | NULL    |                |
| author     | varchar(255)    | NO   |     | NULL    |                |
| created_at | timestamp       | YES  |     | NULL    |                |
| updated_at | timestamp       | YES  |     | NULL    |                |
+------------+-----------------+------+-----+---------+----------------+
5 rows in set (0.00 sec)

## 6. controllerの作成

controllerを作成します。

```terminal
php artisan make:controller BookController --api
```

`app/Http/Controllers/BookController.php`が作成されているので、以下のように修正します。

```php:app/Http/Controllers/BookController.php
<?php

namespace App\Http\Controllers;

use App\Models\Book;
use Illuminate\Http\Request;

class BookController extends Controller
{
    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
        $books = Book::all();
        return response()->json([
            'message' => 'ok',
            'data'    => $books,
        ], 200, [], JSON_UNESCAPED_UNICODE);
    }

    /**
     * Store a newly created resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store(Request $request)
    {
        $book = Book::create($request->all());
        return response()->json([
            'message' => 'Book created successfully',
            'data' => $book
        ], 201, [], JSON_UNESCAPED_UNICODE);
    }

    /**
     * Display the specified resource.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function show($id)
    {
        $book = Book::find($id);
        if ($book) {
            return response()->json([
                'message' => 'ok',
                'data' => $book
            ], 200, [], JSON_UNESCAPED_UNICODE);
        }
        return response()->json([
            'message' => 'Book not found',
        ], 404);
    }

    /**
     * Update the specified resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function update(Request $request, $id)
    {
        $update = [
            'title' => $request->title,
            'author' => $request->author
        ];
        $book = Book::where('id', $id)->update($update);
        if ($book) {
            return response()->json([
                'message' => 'Book updated successfully',
            ], 200);
        }
        return response()->json([
            'message' => 'Book not found',
        ], 404);
    }

    /**
     * Remove the specified resource from storage.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function destroy($id)
    {
        $book = Book::where('id', $id)->delete();
        if ($book) {
            return response()->json([
                'message' => 'Book deleted successfully',
            ], 200);
        }
        return response()->json([
            'message' => 'Book not found',
        ], 404);
    }
}
```

## 7. routesの設定

`routes/api.php`を修正します。

```php:routes/api.php
<?php

use App\Http\Controllers\BookController;
use Illuminate\Support\Facades\Route;

Route::apiResource('/books', BookController::class);
```

# 動作確認

## Laravel起動

```terminal
php artisan serve
Starting Laravel development server: http://127.0.0.1:8000
[Sat Feb 19 17:08:02 2022] PHP 8.1.0 Development Server (http://127.0.0.1:8000) started
```

Laravelが8000番ポートで立ち上がりました。

## Nuxt起動

先ほどと同じです。Laravelが起動した状態でNuxtを起動します。

```terminal
npm run dev

> nuxt-sample@1.0.0 dev
> nuxt

ℹ [HPM] Proxy created: /api  -> http://localhost:8000                                        17:08:29

   ╭───────────────────────────────────────╮
   │                                       │
   │   Nuxt @ v2.15.8                      │
   │                                       │
   │   ▸ Environment: development          │
   │   ▸ Rendering:   client-side          │
   │   ▸ Target:      static               │
   │                                       │
   │   Listening: http://localhost:3000/   │
   │                                       │
   ╰───────────────────────────────────────╯

ℹ Preparing project for development                                                          17:08:31
ℹ Initial build may take a while                                                             17:08:31
ℹ Discovered Components: .nuxt/components/readme.md                                          17:08:31
✔ Builder initialized                                                                        17:08:31
✔ Nuxt files generated                                                                       17:08:31

✔ Client
  Compiled successfully in 3.28s

ℹ Waiting for file changes                                                                   17:08:35
ℹ Memory usage: 209 MB (RSS: 371 MB)                                                         17:08:35
ℹ Listening on: http://localhost:3000/                                                       17:08:35
No issues found.                                                                             17:08:36
```

立ち上がりました。

## ブラウザで確認

http://localhost:3000/ にアクセスします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65508/afb4f6b5-8090-e5c6-0c9f-3a9fb3d43308.png)

表示されたのでCRUD操作をします。

（元サイトの通りなので省略、、、）

DBの値を確認します。

```terminal
mysql -h127.0.0.1 -uroot -ppassword laravel
mysql> SELECT * FROM books;
+----+-----------------------+-----------------------+---------------------+---------------------+
| id | title                 | author                | created_at          | updated_at          |
+----+-----------------------+-----------------------+---------------------+---------------------+
|  2 | 京都大原三千院        | テストユーザー        | 2022-02-19 17:13:07 | 2022-02-19 17:13:10 |
|  3 | iPhoneの使い方        | foo bar               | 2022-02-19 17:14:25 | 2022-02-19 17:14:25 |
+----+-----------------------+-----------------------+---------------------+---------------------+
2 rows in set (0.00 sec)
```

DBに情報が格納されていることが分かります。

# 最後に

ネットの情報のママに打ち込んでも上手くいかないと言うことは多々あるようです。今回は修正した内容については深く追いませんが、やはり初学者の方には面倒でも公式サイトの記述を読んで頂きたいところです。