Laravel + Vue.js + DDDのすゝめ – axiosでLaravelにHTTPリクエストを送信する

Laravel + Vue.js + DDDのすゝめ – axiosでLaravelにHTTPリクエストを送信する

Laravelで作成したバックエンドのAPIにVueからaxiosを使ってリクエストします。

連載

本記事は複数記事の連載記事の1つです。

この記事に関連するコミット

axiosをインストールする

axiosとは、GETやPOSTのHTTPリクエストを送信してサーバーへデータの取得・送信を行うJavaScriptライブラリです。
VSCodeのRemoteContainer拡張からVueのnodeコンテナを開き、axiosとaxiosのTypeScript型定義をインストールします。

yarn add axios
yarn add @types/axios

axiosをラップしたHttpClientクラスを作成する

axiosを直接コールせずにラップすることで様々なメリットがあります。

  1. ライブラリに直接依存しないため置き換えが容易
  2. HTTPリクエストに関わる認証・ロガーなどの共通処理をまとめられる
  3. テスト時にモックに置き換えやすい

とは言え、axiosは多機能でメジャーなため1の置き換えの優先度は低く、2, 3を目的にする程度にラップします。

import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";

class HttpClient {
  private client: AxiosInstance;

  constructor() {
    const config: AxiosRequestConfig = {
      baseURL: "http://127.0.0.1:8000/api/",
    };

    this.client = axios.create(config);
  }

  /**
   * GETリクエスト
   * @param url URL
   * @param config AxiosRequestConfig
   * @returns AxiosResponse
   */
  get = async <T>(
    url: string,
    config: AxiosRequestConfig = {}
  ): Promise<AxiosResponse<T>> => {
    return await this.client.get<T>(url, config);
  };

  /**
   * POSTリクエスト
   * @param url URL
   * @param data POSTデータ
   * @param config AxiosRequestConfig
   * @returns AxiosResponse
   */
  post = async <T>(
    url: string,
    data: any = undefined,
    config: AxiosRequestConfig = {}
  ): Promise<AxiosResponse<any>> => {
    return await this.client.post<T>(url, data, config);
  };

  /**
   * PUTリクエスト
   * @param url URL
   * @param data PUTデータ
   * @param config AxiosRequestConfig
   * @returns AxiosResponse
   */
  put = async <T>(
    url: string,
    data: any = undefined,
    config: AxiosRequestConfig = {}
  ): Promise<AxiosResponse<any>> => {
    return await this.client.put<T>(url, data, config);
  };

  /**
   * DELETEリクエスト
   * @param url URL
   * @param config AxiosRequestConfig
   * @returns AxiosResponse
   */
  delete = async <T>(
    url: string,
    config: AxiosRequestConfig = {}
  ): Promise<AxiosResponse<any>> => {
    return await this.client.delete<T>(url, config);
  };
}

// singleton
const httpClient = new HttpClient();

export { httpClient };

HttpClientクラスに一元化されることで、今後以下のような拡張を行う場合はこのクラスに手を入れるだけでOKです。

拡張例 認証トークンの付与

JWTやOAuth2.0などのリクエストヘッダーに認証トークンを付与する場合はHttpClient内でセットすることで共通化できます。

拡張例 ロガーの追加

axiosはinterceptors.request.useinterceptors.response.useを使用するとリクエスト前後に任意の処理を定義できます。
クライアントのリクエストやレスポンスのログを取得する場合に便利です。

APIリクエストクラスを作成する

サンプルの注文API(orderApi.ts)に先ほどのHttpClientクラスを使ってリクエストするクラスを作成します。
Vueファイルのsetupから直接HTTPリクエストをすることも出来ますが、APIエンドポイントは他の機能からもコールするので再利用できるように分けます。

また、Vueファイルはコンポーネントのレイアウトを担うことが責務なので「注文APIをコールする」以上の「注文APIのエンドポイントのURLやリクエスト方法、レスポンスの受け取り方」といったロジックの知識はVueファイルで持つべきではないと考えます。

import { Order } from "../../models/order";
import { httpClient } from "../../services/httpClient";

export const findOrder = async (orderId: number): Promise<Order | null> => {
  const response = await httpClient.get<Order>(`orders/${orderId}`);

  return new Order(response.data);
};

type CreateOrderResponse = {
  orderId: string;
};

export const createOrder = async (
  orderNumber: string
): Promise<CreateOrderResponse> => {
  const response = await httpClient.post<CreateOrderResponse>("orders", {
    orderNumber: orderNumber,
  });

  return response.data;
};

TypeScriptで型定義をしつつaxiosを使う

axiosはジェネリクスを使用することでレスポンスの型を指定できます。
APIはアプリケーション外の領域で特に返ってくる値に保証がないため、レスポンスを受け取った時点で問題があれば型エラーとして検知できるように型を定義しておくと安全です。

サンプルコードでは以下の<Order>の部分で指定しています。

ジェネリクスは利用するクラス外からクラス内で指定されている型をを指定できます。
これ以上の解説は一言では済まないので別途ググって下さい。

const response = await httpClient.get<Order>(`orders/${orderId}`);

レスポンスの型を定義する

APIからの戻り値の型も定義します。
静的な型付けに慣れていないと面倒に感じるかもしれません。
仮にこのメソッドを初めて使おうと思った時、戻り値が未指定またはanyになっていたとしたら使えるでしょうか?
APIドキュメントを確認しないと使えず、補完も効かず、タイプミスをしてもビルドエラーにならずに通ってしまうため、TypeScriptのメリットが失われてしまいます。

type CreateOrderResponse = {
  orderId: string;
};

モデルの型を定義する

フロントエンドも注文のデータモデルを定義します。

export class Order {
  id: number | null;
  orderNumber: string;
  orderDatetime: Date;

  constructor(order: Partial<Order> = {}) {
    this.id = order.id ?? null;
    this.orderNumber = order.orderNumber ?? "";
    this.orderDatetime = order.orderDatetime ?? new Date();
  }
}

実行確認

今回、Webサーバーは用意していないため、フロントエンド・バックエンド共に開発サーバーを実行する必要があります。

VSCodeからLaravelのPHPコンテナをRemoteContainerで開き、php artisan serveから開発サーバーを起動します。
新しいウィンドウでVSCodeを起動し、VueのnodeコンテナをRemoteContainerで開き、yarn devから開発サーバーを起動します。

※VSCodeのRemoteContainerがホスト・コンテナ間のポートを解決してくれるため設定は必要ありませんが、RemoteContainerを使用しない場合は別途ポート設定が必要です。

取りあえずトップページに最小限の注文の作成・参照するサンプルから動作を確認します。

新規追加

注文番号を入力して新規追加します。
(もう少しそれっぽい項目用意しておけばよかった

Chromeの開発者ツールのネットワークタブから入力した注文番号をパラメータにリクエストが実行されたことが確認できます。

プレビュー(またはレスポンス)を確認すると新しく作成された注文のIDが返却された事を確認できました。

参照

注文ID: 1の注文情報をリクエストした結果が表示されます。
(引数で受け取った注文IDを取得・表示するコンポーネントにしているため、ID: 1固定です)

注文IDはオートインクリメントで採番され、注文番号は入力値、注文日時はデータを起こした日時が入ったことが確認できます。
(書いていて自分でも注文日時=データの作成日時というのは間違いだと思います…

ネットワークからリクエスト・レスポンスが確認できます。

プログラミングカテゴリの最新記事