🕌

Nuxt + Laravel でとりあえず動くものをサクッと作る

2021/12/18に公開
2

本記事は Nuxt + Laravel で とりあえず動くものをサクッと作ろう をテーマにつらつら書いていきます。

はじめに

記事投稿者はフロント実装者ですが、イチから REST APIを構築してみたいなぁ、ということから手順などを記事にしてみました。

こんな方向けの記事です

  • Vue.js + TypeScript の基礎を理解されている方
  • PHP / MySQL の基礎を理解されている方
  • フロントエンドだけどREST APIでとりあえず動くものを作ってみたい方
  • CORSに悩んでいる方
  • 環境変数の持たせ方に悩んでいる方

今回のゴール

  • 簡単なCRUDを実装
    • 今回は書籍管理システム
  • バックエンドはLaravelでREST API構築
  • Nuxtでフロントの構築
  • CORS対策もするよ

環境

  • PC: MacBook Pro (Intel Core 2016)
  • OS: macOS Montery12.0.1
  • フロントエンド:
    • Node.js 16.9.1
    • npm 7.21.1
    • Nuxt.js 2.15.8
    • TypeScript 4.5.4
  • バックエンド
    • PHP 8.0.10
    • Laravel Framework 8.76.2
    • Laravel Installer 4.1.1
  • DB
    • MySQL 14.14

フォルダ構成

全体的な構成です。

- sample
    - nuxt-sample
        - pages
            - index.vue
            - detail
                - _id.vue
        - service
            - book.ts
    - laravel-sample

Nuxt(フロントエンド)の構築

フロントは Nuxt + TypeScriptで構築いたします。

1. create

まずは create コマンドから叩きます。

% mkdir sample
% cd sample
% npx create-nuxt-app <project-name>

npx については以下の記事が参考になったので載せておきます。

https://qiita.com/tonkotsuboy_com/items/8227f5993769c3df533d

コマンドを叩くと対話式でいくつか質問があります。
今回は以下のようにしました。

? 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 using 
typescript)
? Continuous integration: None
? Version control system: Git

インストール完了したらこんなメッセージが出ます。

🎉  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.jsのディレクトリに移動します。

% cd nuxt-sample

一応Nuxt起動してみます。

% npm run dev

うまくいくと以下のように表示されます。

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

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

✔ Client
  Compiled successfully in 8.19s

ℹ Waiting for file changes                                                                                                                                                                          06:34:51
ℹ Memory usage: 249 MB (RSS: 355 MB)                                                                                                                                                                06:34:51
ℹ Listening on: http://localhost:3000/                                                                                                                                                              06:34:51
No issues found.  

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

Screen Shot 2021-12-18 at 6.37.16.png

はい、動いていますね。

2. 開発準備

ここからDX向上のために色々設定していきます。

VSCodeの設定

まず local 用の設定ファイルを作成します。

% mkdir .vscode
% touch .vscode/settings.json

投稿者がいつもやっている設定は以下の通りです。

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の設定

ESLint は rules の箇所だけ以下の通りにしています。

.eslintrc.js
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 のインストールと nuxt.config.js を設定していきます。

まず proxy です。

% npm i @nuxtjs/proxy

次は、nuxt.config.js で proxy の設定を行います。

以下のように修正します。

nuxt.config.js
modules: ['@nuxtjs/axios', '@nuxtjs/proxy'],
axios: { proxy: true },
proxy: {
  '/api': 'http://127.0.0.1:8000',
},

今回は実装を軽くしているので、proxy 内の URL を直書きしていますが、
実務では環境変数を用意します。

上記のコードだと上司に怒られます。

環境変数の設定については著者がQiita側で記述した以下の記事をご参考下さい。

https://qiita.com/tak001/items/dc35d1835fcaa98cd102

4. HTTPClient

今回は axios が入っているので、axios を使います。

外部通信のメソッド作成

外部と通信するメソッドを作成していきます。

軽微なレイヤーにしていますが、
このようなアーキテクチャでは上司に怒られ(ry

% mkdir service
% touch service/book.ts
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 {
  async fetchBooks(): Promise<BookResponse[]> {
    const { data } = await axios.get<AxiosResponse<BookResponse[]>>(
      '/api/books'
    )
    return data.data
  }

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

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

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

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

5. vueファイルの作成

ここからは、vueファイルを作成していきます。

まずは、「pages/index.vue」 から。

pages/index.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.postBook({ ...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>

pages/book/detail/_id.vue

続いては、「pages/book/detail/_id.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>

今回、実装を軽量にするためstoreは使っていません。

はい、ここでフロントは終了です。

Laravel(バックエンド)の構築

続いては、バックエンドを構築していきます。

残念ながら、今回はDockerは使いません。

Dockerでの構築は別記事で紹介したいと思います。

(ちなみに投稿者の mac には phpenv を入れています)

1. create

先ほどまで作成していたフロントのフォルダとは別のフォルダで作っていきます。

% cd sample
% laravel new <project_name>

2. Local に DB を作る

さて、続いては Local に DB を作りましょう。

% mysql -u root
mysql > CREATE DATABASE <db_name>;
mysql > show databases;

上記コマンドで先ほど作成した DB があればOKです。

OKでしたら、以下コマンドで一旦 MySQL から抜けます。

% exit

3. 各種設定

これから、動作のために必要な設定を行なっていきます。

.env

先ほど作成した DB を .env ファイルで設定します。

デフォルトの値はこのようになっています。

L14 あたりです。

DB_DATABASE=laravel_sample
DB_USERNAME=root
DB_PASSWORD=

修正が必要であれば、修正しましょう。

config/app.php

続いては、config/app.php の timezonelocale を以下のように修正します。

L70 あたりと、L83 あたりです。

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

config/database.php

そして、 config/database.php の mysql 内にある charset callation を以下のように修正します。

L55 あたりです。

config/database.php
'charset' => 'utf8'
'callation' => 'utf8_unicode_ci'

4. model の作成

さあ、ここから model を artisan コマンドで作っていきます。

今回は model と migration を同時に作りますので、以下のコマンドを叩きます。

% php artisan make:model Book --migration

完了したら、ホワイトリストを登録しましょう。

自動生成された、app/Models/Book.php の Bookクラス に以下を追加します。

app/Models/Book.php
protected $fillable = [
    'title',
    'author',
];

続いて、自動生成されたマイグレーションファイルで table 設計を行います。

今回は以下のような設計にします。

database/migrations/yyyy_mm_dd_hhmmss_create_books_table.php/function up()
Schema::create('books', function (Blueprint $table) {
    $table->bigIncrements('id');
    $table->string('title');
    $table->string('author');
    $table->timestamps();
});

5. migrate

では、migration していきます。

Laravel の場合、以下のコマンドを叩くだけです。

% php artisan migrate

.env の DB の設定などを間違えていなければ成功するはずです。

念の為、MySQL を確認してみましょう。

% mysql -u root
mysql > use <database_name>;
mysql > show tables;

ついでに、設計通りに作成されているかチェックしてみます。

mysql > DESC books;
+------------+---------------------+------+-----+---------+----------------+
| Field      | Type                | Null | Key | Default | Extra          |
+------------+---------------------+------+-----+---------+----------------+
| id         | bigint(20) 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)

上記のようになっていると思います。

で、一応 mysql から抜けます。

mysql > quit

では、次に進みましょう。

6. controler の作成

続いては、controler を作成します。

こちらも、Laravel であればコマンドで作成できます。

今回は、REST API なので以下のコマンドを叩いてみましょう。

% php artisan make:controller BookController --api

app/Http/Controllers/BookController.php が作成されます。

BookController のメソッドの中身を作る

app/Http/Controllers/BookController.php を見るとメソッドの中身までは
ありませんのでメソッドの中身を作っていきます。

まずは、Book Model を使うので、L6 あたりに use App\Models\Book; を追加します。

index

app/Http/Controllers/BookController.php
public function index()
{
    $books = Book::all();
    return response()->json([
        'message' => 'ok',
        'data' => $books
    ], 200, [], JSON_UNESCAPED_UNICODE);
}

Laravel の ORM(オブジェクト・リレーショナル・マッパー) については以下の公式を参照下さい。

https://readouble.com/laravel/8.x/ja/eloquent.html

store

app/Http/Controllers/BookController.php
public function store(Request $request)
{
    $book = Book::create($request->all());
    return response()->json([
        'message' => 'Book created successfully',
        'data' => $book
    ], 201, [], JSON_UNESCAPED_UNICODE);
}

show

app/Http/Controllers/BookController.php
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

app/Http/Controllers/BookController.php
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);
}

destroy

app/Http/Controllers/BookController.php
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);
}

これで簡単な CRUD が実装できました。

7. routes の作成

続いては、routes/api.php を以下のようにします。

routes/api.php
Route::apiResource('/books', 'App\Http\Controllers\BookController');

今回は、middlewareなどは使わないのでついでに削除しておきます。

8. laravel-cors のインストール

以下のコマンドを叩きます。

% composer require fruitcake/laravel-cors
% php artisan vendor:publish --tag="cors"

はい、これでバックエンドは準備できましたのでいよいよ動かしてみましょう!

動作確認

では、実際に動かしてみます。

Laravel 起動

最初に、Lravael 側を立ち上げます。

Laravel 直下で以下のコマンドを叩きます。

php artisan serve

成功すると以下のような表示が出ます。

Starting Laravel development server: http://127.0.0.1:8000
[Sat Dec 18 15:03:49 2021] PHP 8.0.10 Development Server (http://127.0.0.1:8000) started

これで、バックエンドが port8000 で立ち上がりました。

Nuxt 起動

Laravel 側は動作させたままで、次は Nuxt 側を起動します。

Nuxt 直下で以下のコマンドを叩きます。

npm run dev

build されたらブラウザで確認してみましょう。

ブラウザで確認

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

表示されました。

では、CRUD 操作をしてみましょう。

title と author を入力して「追加」ボタンをクリック。

追加できました。

続いて、削除ボタンを押してみましょう。

削除できました。

では最後に更新をしてみましょう。

「詳細」ボタンをクリックします。

内容を変更して「修正」ボタンを押すと、

無事修正もできました。

これで Nuxt + Laravel で CRUD 操作ができました。

せっかくなので最後に DB の値も見てみましょう。

% myqsl -u root
mysql > SELECT * FROM books;

DB の中身も見ることができました。

以上となります。

Discussion

LOTOyamaguchiLOTOyamaguchi

すみません、質問です。 npm run devをしたところ
WARN in ./pages/book/detail/_id.vue?vue&type=script&lang=ts& friendly-errors 17:58:53
"export 'default' (imported as 'bookService') was not found in '@/service/book'
friendly-errors 17:58:53
と出まして、画面も表示されませんでしたがバージョンや設定の問題でしょうか?

と思ったら、qiitaのほうに修正版上がってましたね。https://qiita.com/uehatsu/items/19b1ae0c706993d0e550 お騒がせしました