Orvalを使ってOpenApiドキュメントからHooksを作成してみる
はじめに
Orvalという、OpenApiのドキュメントからHooksを自動生成してくれるツールを見つけました。
ツールを使わなければ、httpリクエストのソースコードからHooksの作成までのコードを作成して、
メンテしなければならないところを自動生成してくれます。
他のことに集中でき、ミスも減るので是非使ってみたいなと思いました!!😆
ということで、今回はNext.jsのプロジェクトにOrvalのツールを適用してみました🐠
組み込むプロジェクトを作成します
既存のプロジェクトがあればそれを使います。
src
フォルダを作成するオプションでプロジェクトを作成します。
npx create-next-app@latest
OpenApiのドキュメントを作成してみます
ユーザーを操作するAPIになります。
Swaggerのエディタに張り付けると表示することができます。
サンプルで作成したOpenApiのドキュメント
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にすることで、生成コマンド実行時に古い生成ファイルを削除するようになります。
client
にswr
を設定することでswrのHooksが生成されます。
swr以外にも、react-queryなども指定することが出来ます。
今回は、swrを使用してみます。
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
/**
* 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サーバーを起動してください。
組み込んでみる
生成したコードにはswrとaxiosが含まれますのでインストールを行います。
npm i swr axios
ページを作成する
以下のように組み込んでみます。
"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>
</>
)
}
このままではリクエスト先を指定できていませんので、設定を行います。
プロバイダを作成・適応する
設定を行うプロバイダーコンポーネントを作成します。
"use client"
import axios from "axios"
export default function AxiosProvider({
children,
}: {
children: React.ReactNode
}) {
axios.defaults.baseURL="http://localhost:8080"
return children
}
ルートの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を使った記事
Discussion