Open20

(挑戦)swagger-typescript-apiを用いてAPIクライアントを自動生成する

Tsubura TamashiroTsubura Tamashiro

①yamlファイルから自動生成する

  • 用意したyamlファイルからAPIの型を自動生成します。
    • ./index.yamlから自動生成したファイルをsrc配下に出力する(以下画像参考)
  • 以下のコマンドを実行する
npx swagger-typescript-api -p ./index.yaml -o ./src -n testApi.ts  

使用方法ドキュメント

  • これにより自動生成が完了。
  • ここからAPIクライアントを実装していく。
Tsubura TamashiroTsubura Tamashiro

②HttpClientが生成されている。

  • HTTPリクエストを送信するためのメソッド。さまざまなリクエストパラメータと設定を受け取り、適切なフォーマットでリクエストを送信するコードが含まれるー!
export class HttpClient<SecurityDataType = unknown> {
  public instance: AxiosInstance;
  private securityData: SecurityDataType | null = null;
  private securityWorker?: ApiConfig<SecurityDataType>['securityWorker'];
  private secure?: boolean;
  private format?: ResponseType;
Tsubura TamashiroTsubura Tamashiro

③Apiクラスの定義が生成されている。

  • このクラスは、HttpClient<SecurityDataType>を継承しており、HTTPリクエストをさらに具体的なエンドポイントごとに簡潔なメソッドとして定義している。
profiles = {
  // プロフィール情報取得APIの定義
  profilesList: (params: RequestParams = {}) =>
    this.request<
      ProfileResponse,
      BadRequestResponse | UnauthorizedResponse | void | InternalServerErrorResponse
    >({
      path: `/profiles`,
      method: 'GET',
      format: 'json',
      ...params,
    }),

  // プロフィール更新APIの定義
  profilesUpdate: (data: PutProfileRequest, params: RequestParams = {}) =>
    this.request<
      ProfileResponse,
      BadRequestResponse | UnauthorizedResponse | void | InternalServerErrorResponse
    >({
      path: `/profiles`,
      method: 'PUT',
      body: data,
      type: ContentType.Json,
      format: 'json',
      ...params,
    }),
};

  • 以上を用いてAPIクライアントコードを書く。
import axios from 'axios';
import { Api } from './Api'; // Apiクラスの定義をインポート

// 新しいApiクライアントのインスタンスを作成
const api = new Api();

// APIを呼び出す例
async function fetchProfile() {
  try {
    // profilesListメソッドを使用してプロフィール情報を取得
    const response = await api.profiles.profilesList();

    // レスポンスのデータを取得
    const profileData = response.data;

    console.log('Profile Data:', profileData);
  } catch (error) {
    console.error('Error fetching profile:', error);
  }
}

async function updateProfile(updatedProfileData) {
  try {
    // profilesUpdateメソッドを使用してプロフィール情報を更新
    const response = await api.profiles.profilesUpdate(updatedProfileData);

    // 更新後のプロフィール情報を取得
    const updatedProfile = response.data;

    console.log('Updated Profile:', updatedProfile);
  } catch (error) {
    console.error('Error updating profile:', error);
  }
}

Tsubura TamashiroTsubura Tamashiro

APIクライアントの雛形を作る SWR使って。

import React from 'react';
import useSWR from 'swr';
import axios from 'axios';

// APIエンドポイントのベースURL
const API_BASE_URL = 'https://api.example.com';

// SWRのフェッチャーメソッドを定義
const fetcher = async (url) => {
  const response = await axios.get(url);
  return response.data;
};

// SWRを使用してデータを取得するカスタムフック
function useApi(endpoint) {
  const url = `${API_BASE_URL}${endpoint}`;
  const { data, error } = useSWR(url, fetcher);

  return {
    data,
    isLoading: !error && !data,
    isError: error,
  };
}

// アプリケーションコンポーネント
function App() {
  // プロフィール情報の取得
  const profile = useApi('/profiles');

  // プロフィール情報を表示
  return (
    <div>
      <h1>Profile Data</h1>
      {profile.isLoading && <p>Loading...</p>}
      {profile.isError && <p>Error: {profile.isError.message}</p>}
      {profile.data && (
        <div>
          <p>Name: {profile.data.name}</p>
          <p>Email: {profile.data.email}</p>
          {/* 他のプロフィール情報を表示 */}
        </div>
      )}
    </div>
  );
}

export default App;

Tsubura TamashiroTsubura Tamashiro

そもそもSWRって何?

  • データ取得と状態管理のためのライブラリ
  • SWR(Stale-While-Revalidate0) vercelがサポートしている。vercel色々やっててすごい

コンセプト

  • データを「古い状態のままで表示しつつ、再取得を行う」アプローチを取る。最初のデータ取得時にはキャッシュされたデータ(もしあれば)を即座に表示し、同時にバックグラウンドでデータの再取得を行う。
Tsubura TamashiroTsubura Tamashiro

SWR使う手順

  • SWRライブラリをインストール
yarn add swr
  • SWRフック(通常はuseSWR)を使用して、データ取得と状態管理を行うコンポーネント内でデータ取得ロジックを記述。
  • SWRフックにAPIエンドポイントなどのデータソースを指定し、取得したデータや状態情報をコンポーネント内で利用。
Tsubura TamashiroTsubura Tamashiro

SWR まとめ

①始めに

yarn add swr
  • fetcher関数を作る
const fetcher = (...args) => fetch(...args).then(res => res.json())
  • クライアントで使用
import useSWR from 'swr'
 
function Profile () {
  const { data, error, isLoading } = useSWR('/api/user/123', fetcher)
 
  if (error) return <div>failed to load</div>
  if (isLoading) return <div>loading...</div>
 
  // データをレンダリングする
  return <div>hello {data.name}!</div>
}

再利用を可能にする

  • 例えばユーザのデータ取る時
function useUser (id) {
  const { data, error, isLoading } = useSWR(`/api/user/${id}`, fetcher)
 
  return {
    user: data,
    isLoading,
    isError: error
  }
}
  • コンポーネント内に渡す。
function Avatar ({ id }) {
  const { user, isLoading, isError } = useUser(id)
 
  if (isLoading) return <Spinner />
  if (isError) return <Error />
  return <img src={user.avatar} />
}
Tsubura TamashiroTsubura Tamashiro

②API

const { data, error, isLoading, isValidating, mutate } = useSWR(key, fetcher, options)

パラメーター

  • useSWRの第1引数「key」
    • リクエストのためのユニークなキー文字列(または関数、配列、null )
  • useSWRの第2引数「fetcher」
  • データをフェッチするための Promise を返す関数
  • useSWRの第2引数「option」
    • この SWR フックのオプションオブジェクト

返り値

  • data: fetcher によって解決された、指定されたキーのデータ(もしくは、ロードされていない場合は undefined)
  • error: fetcher によって投げられたエラー (もしくは undefined)
  • isLoading: 実行中のリクエストがあり "ロードされたデータ" がない状態。フォールバックのためのデータや keepPreviousData による以前のデータは "ロードされたデータ" ではありません
  • isValidating: リクエストまたは再検証の読み込みがある場合
  • mutate(data?, options?): キャッシュされたデータを更新する関数 ( 詳細 )

オプション

ドキュメント

Tsubura TamashiroTsubura Tamashiro

③データフェッチ

const { data, error } = useSWR(key, fetcher)
  • SWR のとても基本的な API 。この fetcher は非同期関数であり、SWR の key を受け取りデータを返す。

axios

import axios from 'axios'
 
const fetcher = url => axios.get(url).then(res => res.data)
 
function App () {
  const { data, error } = useSWR('/api/data', fetcher)
  // ...
}
Tsubura TamashiroTsubura Tamashiro

axiosを用いたSWRのhooks雛形作り

  • ①ベースとなるエンドポイント定義する。
  • ②SWRの第二引数に入れるfetcherメソッド定義する。
  • ③SWRを用いてデータ取得するカスタムフックを定義する。
  • ④使いたいコンポーネントにそれぞれ適用する。

①ベースとなるエンドポイントを定義する

  • これは最初に読み込まれるファイルに定義する。私はapp routerの以前のnextでの作業だったので_app.tsxに記述する。
// APIエンドポイントのベースURL
const BASE_URL = 'https://api.example.com';

②SWRの第二引数に入れるfetcherを定義する

import axios from 'axios';
import useSWR from 'swr';
import { Api } from './path-to-generated-api'; // 自動生成されたApiクラスのインポート

// Apiクラスのインスタンスを作成
const api = new Api();

// カスタムフックを定義
export const useApi = ({ method, url, token, params }: Props) => {
  // データを取得するための関数
  const fetcher = async (
    method: string,
    url?: string,
    token?: string,
    params?: { [k: string]: string }
  ) => {
    if (!url || !token) {
      return;
    }
    // 自動生成されたApiクラスを使用してデータを取得
    const response = await api.request({
      method: method,
      url: url,
      data: params,
      headers: token ? { Authorization: `Bearer ${token}` } : {},
    });
    return response.data;
  };

  // SWRを使用してデータを取得
  const { data, error } = useSWR(
    [url, token],
    async () => await fetcher(method, url, token, params),
    {
      onErrorRetry: (error, _key, _config, revalidate, { retryCount }) => {
        // 404と401では再試行しない。
        if (error.response.status === 404 || error.response.status === 401) {
          return;
        }

        // 再試行は10回までしかできません。
        if (retryCount >= 10) {
          return;
        }

        // 5秒後に再試行します。
        setTimeout(() => revalidate({ retryCount }), 5000);
      },
    }
  );

  return {
    data,
    isLoading: !error && !data,
    isError: error,
  };
};

Tsubura TamashiroTsubura Tamashiro

axiosとSWRを用いたAPIクライアントを作成する

import axios from 'axios';
import useSWR from 'swr';
// ----------------------------------------------------------------------

export default function useApi(endpoint: string) {
  const BASE_URL = 'あなたのAPIのURL';
  const fetcher = async (url: string) => {
    const response = await axios.get(url, {
      headers: {
        Authorization: `Bearer ${トークン入れて下さい}`,
      },
    });
    return response.data;
  };

  const url = `${BASE_URL}${endpoint}`;
  const { data, error } = useSWR(url, fetcher);
  return {
    data,
    isLoading: !error && !data,
    isError: error,
  };
}
  • useApiをクライアントで呼び出します。
  const { data, isError } = useApi('/取得したいAPIのURL');
  • これだけでAPIが叩けます!!!
  • axiosとswrに感謝
Tsubura TamashiroTsubura Tamashiro

SWRのTypescriptを使ってみる

ドキュメントを読解していく

  • SWR は TypeScript に対応しており、型の安全性を使える。
  • SWR は key から fetcher の引数の型を推測する。そのため、適切な型を自動的に設定できる。
  • keyとfecherは明示的に型を示すこともできる
import useSWR, { Fetcher } from 'swr'
 
const uid = '<user_id>'
const fetcher: Fetcher<User, string> = (id) => getUserById(id)
 
const { data } = useSWR(uid, fetcher)
// `data` は `User | undefined` となります。
  • デフォルトはanyであるerrorにも型をつけることができる
const { data, error } = useSWR<User, Error>(uid, fetcher);
// `data` は `User | undefined` となります.
// `error` は `Error | undefined` となります.
  • dataの型定義は以下
// 🔹 A. 型付きの fetcher を使う:
// `getUser` は `(endpoint: string) => User` になります。
const { data } = useSWR('/api/user', getUser)
 
// 🔹 B. データ型を指定:
// `fetcher` は 通常 `any` を返します。
const { data } = useSWR<User>('/api/user', fetcher)
Tsubura TamashiroTsubura Tamashiro

SWR | ミューテーションと再検証(fetche)

  • SWR はリモートデータとキャッシュデータのミューテーションのために、mutate と useSWRMutation の API を提供している。
  • mutate API を使いデータをミューテート
    • どんなキーに対してもミューテートできるグローバルのミューテートAPI
    • 対応する SWR フックのデータのみミューテートできるバウンドミューテート API

グローバルミューテートAPI

import { useSWRConfig } from "swr"
 
function App() {
  const { mutate } = useSWRConfig()
  mutate(key, data, options)
}
  • グローバルにimportも可能
import { mutate } from "swr"
 
function App() {
  mutate(key, data, options)
}

バウンドミューテートAPI

  • 現在のキーのデータをミューテートする最も簡単な方法
  • useSWR に渡された key に対して対応付けられ、data を第一引数として受け取る。
  • 機能自体はグローバルな mutate と同じだが、key を引数として指定する必要がない。
import useSWR from 'swr'
 
function Profile () {
  const { data, mutate } = useSWR('/api/user', fetcher)
 
  return (
    <div>
      <h1>My name is {data.name}.</h1>
      <button onClick={async () => {
        const newName = data.name.toUpperCase()
        // データを更新するために API にリクエストを送信します
        await requestUpdateUsername(newName)
        // ローカルのデータを即座に更新して再検証(再フェッチ)します
        // 注意: useSWR の mutate は key が対応付けられているため、key の指定は必要ありません
        mutate({ ...data, name: newName })
      }}>Uppercase my name!</button>
    </div>
  )
}

再検証(fetch)

  • 以下のようにmutate(key) (または単にバウンドミューテートの mutate()) をデータの指定なしに呼んだ場合、そのリソースに対して再fetchが走る。
mutate('/api/user')
mutate()

APIパラメータ

key

  • useSWR の key と同じです。しかしながら、関数は フィルタ関数 として振る舞います

data

  • クライアントキャッシュを更新するためのデータ、またはリモートミューテーションのための非同期関数

options

  • 下記のオプションを受け取ります

optimisticData

  • クライアントキャッシュを即座に更新するためのデータ、または現在のデータを受け取り新しいクライアントキャッシュデータを返す関数。

revalidate = true

  • 非同期の更新処理を完了した後にキャッシュの再検証を行うかどうか

populateCache = true

  • リモートミューテーションの結果をキャッシュに書き込むかどうか、またはリモートミューテーションの結果と現在のデータを引数として受け取り、ミューテーションの結果を返す関数

rollbackOnError = true

  • リモートミューテーションがエラーだった場合にキャッシュをロールバックするかどうか、または発生したエラーを引数として受け取りロールバックするかどうかの真偽値を返す関数

throwOnError = true

  • ミューテートの呼び出しが失敗した場合にエラーを投げるかどうか

返り値

  • mutate は data パラメータとして扱われる結果を返す。
  • mutate に渡された関数は対応するキャッシュの値としても使われる更新後のデータを返す。
try {
  const user = await mutate('/api/user', updateUser(newUser))
} catch (error) {
  // ユーザーの更新中に発生したエラーを処理します
}
Tsubura TamashiroTsubura Tamashiro

useSWRMutation

  • 基本的な使い方
import useSWRMutation from 'swr/mutation'
 
async function sendRequest(url, { arg }: { arg: { username: string } }) {
  return fetch(url, {
    method: 'POST',
    body: JSON.stringify(arg)
  }).then(res => res.json())
}
 
function App() {
  const { trigger, isMutating } = useSWRMutation('/api/user', sendRequest, /* options */)
 
  return (
    <button
      disabled={isMutating}
      onClick={async () => {
        try {
          const result = await trigger({ username: 'johndoe' }, /* options */)
        } catch (e) {
          // エラーハンドリング
        }
      }}
    >
      Create User
    </button>
  )
}
  • mutationの結果をレンダリングで使う場合
const { trigger, data, error } = useSWRMutation('/api/user', sendRequest)
  • 読み込みの遅延を実装する
import { useState } from 'react'
import useSWRMutation from 'swr/mutation'
 
const fetcher = url => fetch(url).then(res => res.json())
 
const Page = () => {
  const [show, setShow] = useState(false)
  // trigger が呼ばれるまで data は undefined
  const { data: user, trigger } = useSWRMutation('/api/user', fetcher);
 
  return (
    <div>
      <button onClick={() => {
        trigger();
        setShow(true);
      }}>Show User</button>
      {show && user ? <div>{user.name}</div> : null}
    </div>
  );
}