🐟

Orvalを使ってOpenApiドキュメントからHooksを作成してみる

2023/11/01に公開

はじめに

Orvalという、OpenApiのドキュメントからHooksを自動生成してくれるツールを見つけました。
ツールを使わなければ、httpリクエストのソースコードからHooksの作成までのコードを作成して、
メンテしなければならないところを自動生成してくれます。
他のことに集中でき、ミスも減るので是非使ってみたいなと思いました!!😆

ということで、今回はNext.jsのプロジェクトにOrvalのツールを適用してみました🐠

組み込むプロジェクトを作成します

既存のプロジェクトがあればそれを使います。
srcフォルダを作成するオプションでプロジェクトを作成します。

npx create-next-app@latest

OpenApiのドキュメントを作成してみます

ユーザーを操作するAPIになります。
Swaggerのエディタに張り付けると表示することができます。

サンプルで作成したOpenApiのドキュメント
./openapi/openapi.oas3.yml
openapi: "3.0.3"

info:
  title: "サンプルAPI"
  description: "サンプル用のAPIです"
  version: "0.0.1"

servers:
  - url: "http://localhost:8080"
    description: "ローカル環境"
  - url: "https://sample.com"
    description: "本番環境"

tags:
  - name: "users"
    description: "ユーザー情報"

paths:
  "/users":
    get:
      tags: ["users"]
      summary: "ユーザー一覧取得"
      responses:
        "200":
          description: "成功"
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/User"
        "500":
          description: "内部エラー"
    post:
      tags: ["users"]
      summary: "ユーザー作成"
      requestBody:
        description: "新規ユーザー情報"
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/User"
      responses:
        "201":
          description: "成功"
        "400":
          description: "不正なパラメータ"
        "409":
          description: "パラメータに競合があります"
        "500":
          description: "内部エラー"
  "/users/{discriminator}":
    get:
      tags: ["users"]
      summary: "ユーザー情報取得"
      parameters:
        - name: discriminator
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Discriminator"
      responses:
        "200":
          description: "成功"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"
        "400":
          description: "不正なパラメータ"
        "404":
          description: "該当ユーザーなし"
        "500":
          description: "内部エラー"
    put:
      tags: ["users"]
      summary: "ユーザー情報変更"
      parameters:
        - name: discriminator
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Discriminator"
      requestBody:
        description: "新規ユーザー情報"
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/User"
      responses:
        "204":
          description: "成功"
        "400":
          description: "不正なパラメータ"
        "404":
          description: "該当ユーザーなし"
        "409":
          description: "パラメータに競合があります"
        "500":
          description: "内部エラー"

    delete:
      tags: ["users"]
      summary: "ユーザー削除"
      parameters:
        - name: discriminator
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Discriminator"
      responses:
        "204":
          description: "成功"
        "400":
          description: "不正なパラメータ"
        "404":
          description: "該当ユーザーなし"
        "500":
          description: "内部エラー"

components:
  schemas:
    User:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 24
          example: "ユーザー名"
        email:
          type: string
          format: email
        discriminator:
          $ref: "#/components/schemas/Discriminator"
        note:
          type: string
          maxLength: 256
    Discriminator:
      type: string
      pattern: "^[A-Za-z0-9_]+$"
      maxLength: 24
      minLength: 3

ライブラリのインストール

Orvalをインストールします

npm i orval -D

設定ファイルの作成

orvalのコンフィグはこちらに記入していきます。
シンプルに入力するOpenApiのドキュメントと出力先のファイルを選択します。
cleanをtrueにすることで、生成コマンド実行時に古い生成ファイルを削除するようになります。
clientswrを設定することでswrのHooksが生成されます。
swr以外にも、react-queryなども指定することが出来ます。
今回は、swrを使用してみます。

./orval.config.ts
import { defineConfig } from 'orval';

export default defineConfig({
  backend: {
    input: {
      target: "./openapi/openapi.oas3.yml",
    },
    output: {
      target: "./src/api/backend.ts",
      clean: true,
      client: "swr",
    },
  },
});

Hooksの生成

以下のコマンドを実行して生成します。

npx orval

これでbackendへのリクエストのコードから、型、Hooksまでを生成してくれます。
すごく楽ですし、ミスが発生しないのが素晴らしいです😭

生成したHooks
./src/api/backend.ts
/**
 * Generated by orval v6.19.1 🍺
 * Do not edit manually.
 * サンプルAPI
 * サンプル用のAPIです
 * OpenAPI spec version: 0.0.1
 */
import {
  useMutation,
  useQuery
} from '@tanstack/react-query'
import type {
  MutationFunction,
  QueryFunction,
  QueryKey,
  UseMutationOptions,
  UseQueryOptions,
  UseQueryResult
} from '@tanstack/react-query'
import axios from 'axios'
import type {
  AxiosError,
  AxiosRequestConfig,
  AxiosResponse
} from 'axios'
export type Discriminator = string;

export interface User {
  discriminator?: Discriminator;
  email?: string;
  name?: string;
  note?: string;
}





/**
 * @summary ユーザー一覧取得
 */
export const getUsers = (
     options?: AxiosRequestConfig
 ): Promise<AxiosResponse<User[]>> => {
    
    return axios.get(
      `/users`,options
    );
  }


export const getGetUsersQueryKey = () => {
    
    return [`/users`] as const;
    }

    
export const getGetUsersQueryOptions = <TData = Awaited<ReturnType<typeof getUsers>>, TError = AxiosError<void>>( options?: { query?:UseQueryOptions<Awaited<ReturnType<typeof getUsers>>, TError, TData>, axios?: AxiosRequestConfig}
) => {

const {query: queryOptions, axios: axiosOptions} = options ?? {};

  const queryKey =  queryOptions?.queryKey ?? getGetUsersQueryKey();

  

    const queryFn: QueryFunction<Awaited<ReturnType<typeof getUsers>>> = ({ signal }) => getUsers({ signal, ...axiosOptions });

      

      

   return  { queryKey, queryFn, ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof getUsers>>, TError, TData> & { queryKey: QueryKey }
}

export type GetUsersQueryResult = NonNullable<Awaited<ReturnType<typeof getUsers>>>
export type GetUsersQueryError = AxiosError<void>

/**
 * @summary ユーザー一覧取得
 */
export const useGetUsers = <TData = Awaited<ReturnType<typeof getUsers>>, TError = AxiosError<void>>(
  options?: { query?:UseQueryOptions<Awaited<ReturnType<typeof getUsers>>, TError, TData>, axios?: AxiosRequestConfig}

  ):  UseQueryResult<TData, TError> & { queryKey: QueryKey } => {

  const queryOptions = getGetUsersQueryOptions(options)

  const query = useQuery(queryOptions) as  UseQueryResult<TData, TError> & { queryKey: QueryKey };

  query.queryKey = queryOptions.queryKey ;

  return query;
}


/**
 * @summary ユーザー作成
 */
export const postUsers = (
    user: User, options?: AxiosRequestConfig
 ): Promise<AxiosResponse<void>> => {
    
    return axios.post(
      `/users`,
      user,options
    );
  }



export const getPostUsersMutationOptions = <TError = AxiosError<unknown>,
    
    TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof postUsers>>, TError,{data: User}, TContext>, axios?: AxiosRequestConfig}
): UseMutationOptions<Awaited<ReturnType<typeof postUsers>>, TError,{data: User}, TContext> => {
 const {mutation: mutationOptions, axios: axiosOptions} = options ?? {};

      


      const mutationFn: MutationFunction<Awaited<ReturnType<typeof postUsers>>, {data: User}> = (props) => {
          const {data} = props ?? {};

          return  postUsers(data,axiosOptions)
        }

        


   return  { mutationFn, ...mutationOptions }}

    export type PostUsersMutationResult = NonNullable<Awaited<ReturnType<typeof postUsers>>>
    export type PostUsersMutationBody = User
    export type PostUsersMutationError = AxiosError<unknown>

    /**
 * @summary ユーザー作成
 */
export const usePostUsers = <TError = AxiosError<unknown>,
    
    TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof postUsers>>, TError,{data: User}, TContext>, axios?: AxiosRequestConfig}
) => {

      const mutationOptions = getPostUsersMutationOptions(options);

      return useMutation(mutationOptions);
    }
    
/**
 * @summary ユーザー情報取得
 */
export const getUsersDiscriminator = (
    discriminator: Discriminator, options?: AxiosRequestConfig
 ): Promise<AxiosResponse<User>> => {
    
    return axios.get(
      `/users/${discriminator}`,options
    );
  }


export const getGetUsersDiscriminatorQueryKey = (discriminator: Discriminator,) => {
    
    return [`/users/${discriminator}`] as const;
    }

    
export const getGetUsersDiscriminatorQueryOptions = <TData = Awaited<ReturnType<typeof getUsersDiscriminator>>, TError = AxiosError<void>>(discriminator: Discriminator, options?: { query?:UseQueryOptions<Awaited<ReturnType<typeof getUsersDiscriminator>>, TError, TData>, axios?: AxiosRequestConfig}
) => {

const {query: queryOptions, axios: axiosOptions} = options ?? {};

  const queryKey =  queryOptions?.queryKey ?? getGetUsersDiscriminatorQueryKey(discriminator);

  

    const queryFn: QueryFunction<Awaited<ReturnType<typeof getUsersDiscriminator>>> = ({ signal }) => getUsersDiscriminator(discriminator, { signal, ...axiosOptions });

      

      

   return  { queryKey, queryFn, enabled: !!(discriminator), ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof getUsersDiscriminator>>, TError, TData> & { queryKey: QueryKey }
}

export type GetUsersDiscriminatorQueryResult = NonNullable<Awaited<ReturnType<typeof getUsersDiscriminator>>>
export type GetUsersDiscriminatorQueryError = AxiosError<void>

/**
 * @summary ユーザー情報取得
 */
export const useGetUsersDiscriminator = <TData = Awaited<ReturnType<typeof getUsersDiscriminator>>, TError = AxiosError<void>>(
 discriminator: Discriminator, options?: { query?:UseQueryOptions<Awaited<ReturnType<typeof getUsersDiscriminator>>, TError, TData>, axios?: AxiosRequestConfig}

  ):  UseQueryResult<TData, TError> & { queryKey: QueryKey } => {

  const queryOptions = getGetUsersDiscriminatorQueryOptions(discriminator,options)

  const query = useQuery(queryOptions) as  UseQueryResult<TData, TError> & { queryKey: QueryKey };

  query.queryKey = queryOptions.queryKey ;

  return query;
}


/**
 * @summary ユーザー情報変更
 */
export const putUsersDiscriminator = (
    discriminator: Discriminator,
    user: User, options?: AxiosRequestConfig
 ): Promise<AxiosResponse<void>> => {
    
    return axios.put(
      `/users/${discriminator}`,
      user,options
    );
  }



export const getPutUsersDiscriminatorMutationOptions = <TError = AxiosError<unknown>,
    
    TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof putUsersDiscriminator>>, TError,{discriminator: Discriminator;data: User}, TContext>, axios?: AxiosRequestConfig}
): UseMutationOptions<Awaited<ReturnType<typeof putUsersDiscriminator>>, TError,{discriminator: Discriminator;data: User}, TContext> => {
 const {mutation: mutationOptions, axios: axiosOptions} = options ?? {};

      


      const mutationFn: MutationFunction<Awaited<ReturnType<typeof putUsersDiscriminator>>, {discriminator: Discriminator;data: User}> = (props) => {
          const {discriminator,data} = props ?? {};

          return  putUsersDiscriminator(discriminator,data,axiosOptions)
        }

        


   return  { mutationFn, ...mutationOptions }}

    export type PutUsersDiscriminatorMutationResult = NonNullable<Awaited<ReturnType<typeof putUsersDiscriminator>>>
    export type PutUsersDiscriminatorMutationBody = User
    export type PutUsersDiscriminatorMutationError = AxiosError<unknown>

    /**
 * @summary ユーザー情報変更
 */
export const usePutUsersDiscriminator = <TError = AxiosError<unknown>,
    
    TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof putUsersDiscriminator>>, TError,{discriminator: Discriminator;data: User}, TContext>, axios?: AxiosRequestConfig}
) => {

      const mutationOptions = getPutUsersDiscriminatorMutationOptions(options);

      return useMutation(mutationOptions);
    }
    
/**
 * @summary ユーザー削除
 */
export const deleteUsersDiscriminator = (
    discriminator: Discriminator, options?: AxiosRequestConfig
 ): Promise<AxiosResponse<void>> => {
    
    return axios.delete(
      `/users/${discriminator}`,options
    );
  }



export const getDeleteUsersDiscriminatorMutationOptions = <TError = AxiosError<unknown>,
    
    TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof deleteUsersDiscriminator>>, TError,{discriminator: Discriminator}, TContext>, axios?: AxiosRequestConfig}
): UseMutationOptions<Awaited<ReturnType<typeof deleteUsersDiscriminator>>, TError,{discriminator: Discriminator}, TContext> => {
 const {mutation: mutationOptions, axios: axiosOptions} = options ?? {};

      


      const mutationFn: MutationFunction<Awaited<ReturnType<typeof deleteUsersDiscriminator>>, {discriminator: Discriminator}> = (props) => {
          const {discriminator} = props ?? {};

          return  deleteUsersDiscriminator(discriminator,axiosOptions)
        }

        


   return  { mutationFn, ...mutationOptions }}

    export type DeleteUsersDiscriminatorMutationResult = NonNullable<Awaited<ReturnType<typeof deleteUsersDiscriminator>>>
    
    export type DeleteUsersDiscriminatorMutationError = AxiosError<unknown>

    /**
 * @summary ユーザー削除
 */
export const useDeleteUsersDiscriminator = <TError = AxiosError<unknown>,
    
    TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof deleteUsersDiscriminator>>, TError,{discriminator: Discriminator}, TContext>, axios?: AxiosRequestConfig}
) => {

      const mutationOptions = getDeleteUsersDiscriminatorMutationOptions(options);

      return useMutation(mutationOptions);
    }

Mockサーバを立てる

こちらの記事を参考にMockサーバーを起動してください。
https://zenn.dev/collabostyle/articles/22660415d80f1d

組み込んでみる

生成したコードにはswrとaxiosが含まれますのでインストールを行います。

npm i swr axios

ページを作成する

以下のように組み込んでみます。

./src/app/page.tsx
"use client"
import { useGetUsers } from '@/api/backend'

export default function Home() {
  const { data: users, error, isLoading } = useGetUsers();

  if(isLoading) {
    return <div>読み込み中</div>
  }

  if (error) {
    return <div>ユーザーの取得に失敗しました</div>
  }

  return (
    <>
      <div>ユーザー</div>
      <table>
        <tbody>
          {
            users?.data.map((user) => {
              return(
              <tr key={user.discriminator}>
                <td>{user.name}</td>
                <td>{user.discriminator}</td>
                <td>{user.email}</td>
                <td>{user.note}</td>
              </tr>
            )
          })
          }
        </tbody>
      </table>
    </>
  )
}

このままではリクエスト先を指定できていませんので、設定を行います。

プロバイダを作成・適応する

設定を行うプロバイダーコンポーネントを作成します。

./src/app/AxiosProvider.tsx
"use client"

import axios from "axios"

export default function AxiosProvider({
  children,
}: {
  children: React.ReactNode
}) {
  axios.defaults.baseURL="http://localhost:8080"

  return children
}

ルートのlayout.tsxにプロバイダーコンポーネントを追加します。

./src/app/layout.tsx
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css'
import AxiosProvider from './AxiosProvider'

const inter = Inter({ subsets: ['latin'] })

export const metadata: Metadata = {
  title: 'Create Next App',
  description: 'Generated by create next app',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  

  return (
    <AxiosProvider>
      <html>
        <body className={inter.className}>{children}</body>
      </html>
    </AxiosProvider>
  )
}

これでリクエスト先の設定完了です。

画面を見るとPrismのMockサーバからデータを取得して、表示がされるようになりました!

おわりに

クライアントのソースコードの生成だけではなく、hooksまで生成してくれるところが素晴らしいと思います😆
今回はswrを使っていきましたが、react-queryを使った場合も見てみたいと思います。

React-Queryを使った記事
https://zenn.dev/collabostyle/articles/b08a64a1d3ad1c

コラボスタイル Developers

Discussion