フロントエンドデベロッパーの村があった。
遥か遠く、リモートワークという名の高い山と、言語やフレームワークの谷を越えたところにはバックエンドデベロッパー達が住んでいる村があるらしい。
Slackでの非同期コミュニケーションと、デイリースクラムといった定例会議でのやり取りのみが彼らとの通信手段であった。
それらの機会によってバックエンドデベロッパーから伝えられたAPIの型定義を、フロントエンドデベロッパー達は/features/user/types
などのディレクトリに手書きで作成していた。
神は仰られた。
APIをバグ無く叩きたければ、祈りを捧げよ
と。
user.profile_image
は、文字列だと聞いていたのにnull
が返ってきた。
post.author_id
は、当然数値だと思っていたが文字列だった。
description
を空文字のままPOSTすると、思いもよらずバリデーションエラーが返却された。
仕様の認識の齟齬により、本番環境では今日も調査依頼が発生する。
雨は降り止まない。
当時、フロントエンドデベロッパー達の間では、それは信仰心が足りないからだと言われていた。
彼らは神に従順であった。
しかし、認知負荷は肥大化し続ける一方である。
ある日、それを堰き止めるダムが決壊した。
フロントエンドデベロッパーの村の歴史は、その幕を閉じるかと思われた。
....芽生えたのである。
Orvalというツールの新芽が。
フロントエンドデベロッパー達は、祈ることをやめた。
バックエンドデベロッパーに協力を仰ぎながら、自らの手で、Orvalを大切に大切に運用していったと言う。
サンプルコード
v0くんと協力して作ったサンプルアプリです。
vite + react-router vs NestJS の構成になっています。
Orval 概要
OpenAPI Generatorの一つです。
OpenAPI Generatorとはそもそも2018年に公開されたOpenAPI Tools製のツールで、OpenAPI Specificationを元にクライアントやサーバのコードを生成するソフトウェアです。
OpenAPI定義(Swagger等です。)から、各種HTTP Clientを使ってのエンドポイントシバキコードを自動生成出来ます。
こんな感じのPost削除エンドポイントに対し、Axiosを指定したときの例です。
// openapi定義
"delete": {
"operationId": "PostsController_remove",
"summary": "Delete a post",
"parameters": [
{
"name": "id",
"required": true,
"in": "path",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "The post has been deleted"
}
},
"tags": [
"posts"
]
}
// 生成されるエンドポイントシバキコード
const postsControllerRemove = (
id,
options,
) => {
return axios.default.delete(`/posts/${id}`, options)
}
これだけでも石器時代に生きるエンドポイントデベロッパー達はさながら火を発見した猿人類かのように狂喜乱舞を始めそうですが、Orvalは上記のエンドポイントシバキコード自動生成のほかに、魅力的な機能が多く実装されています。
公式ガイド冒頭に書かれている通りとなりますが、それを紹介出来ればと感じています。
Generate typescript models
型情報を生成します。
そのくらいはやって欲しいものではありますが、シンプルゆえに強力です。
以下がサンプルコード内のUser Entityです。
import { ApiProperty } from "@nestjs/swagger"
export class User {
@ApiProperty({ example: 1, description: "The user ID" })
id: number
@ApiProperty({ example: "John Doe", description: "The user name" })
name: string
@ApiProperty({ example: "john@example.com", description: "The user email" })
email: string
}
このUserクラスを各所で指定し、OpenAPIドキュメントを生成しています。
Orvalによって、フロント側で生成される型情報が以下になります。
export interface User {
/** The user email */
email: string
/** The user ID */
id: number
/** The user name */
name: string
}
素晴らしいですね。
当然、そのほかDTOも型として出力されています。
Generate HTTP Calls
エンドポイントシバキコード改め、指定したHTTP Clientによる特定エンドポイントに対するアクセス関数になります。
axios、fetchなどは当然ですが、Orvalの凄いところは、Zod、SWR、Vue query、Angular Clientといったモダンフレームワークに隣接するものに対応していることです。
また、それらの全てで、baseUrlの指定、カスタムインスタンスを用いたI/Oのインターセプタ定義も可能です。
@Put(':id')
@ApiOperation({ summary: 'Update a user' })
@ApiResponse({
status: 200,
description: 'The user has been updated',
type: User
})
update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
return this.usersService.update(+id, updateUserDto);
}
上記のようなPresenterがあった場合、OpenAPIドキュメントが正しく生成されていれば、以下のようなシバキ関数が生成されます。
// 自前で設置した customInstance
import { customInstance } from './mutator/custom-instance'
/**
* @summary Update a user
*/
export const usersControllerUpdate = (id: string, updateUserDto: UpdateUserDto) => {
return customInstance<User>({
url: `/users/${id}`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
data: updateUserDto,
})
}
Generate Mocks with MSW
MSWを用いたモックハンドラーを自動生成します。
モックデータも同様です。
データはfaker-jsを使用し、取得のたびに異なるものが返ってきます。
MSWとは?
ブラウザとNode.jsのネットワークレベルでAPIリクエストをインターセプトして、モックのレスポンスを返すライブラリとなります。
実際のAPIエンドポイントを叩いているコードのまま、準備したテストデータを返させることが出来ます。
実際に生成されるモックハンドラーとしては以下のようなものです。
// モックデータの準備
export const getUsersControllerFindAllResponseMock = (): User[] =>
Array.from({ length: faker.number.int({ min: 1, max: 10 }) }, (_, i) => i + 1).map(() => ({
email: faker.word.sample(),
id: faker.number.int({ min: undefined, max: undefined }),
name: faker.word.sample(),
}))
// モックデータを返す、MSW用のハンドラー
export const getUsersControllerFindAllMockHandler = (
overrideResponse?:
| User[]
| ((info: Parameters<Parameters<typeof http.get>[1]>[0]) => Promise<User[]> | User[]),
) => {
return http.get('*/users', async (info) => {
await delay(1000)
return new HttpResponse(
JSON.stringify(
overrideResponse !== undefined
? typeof overrideResponse === 'function'
? await overrideResponse(info)
: overrideResponse
: getUsersControllerFindAllResponseMock(),
),
{
status: 200,
headers: {
'Content-Type': 'application/json',
},
},
)
})
}
これらのMSW用のコードは、public/mockServiceWorker、mocks/browser.tsの二つのファイルの設置のみで動作可能になります。
設定オプション
オプション自体は膨大な量があるため、いくつか特徴的なものをピックアップして紹介させてください。
input.filters
OpenAPIドキュメント中、特定の条件に当てはまったエンドポイントのみをOrval自動生成の対象とするオプションです。
tagsやschemasを文字で指定可能です。
input.target
対象のOpenAPIファイルの位置です。
エンドポイントを指定することも出来ます。
input: {
target: 'http://localhost:3001/api-json',
},
output.target
生成されるorvalを配置するパスです。
module.exports = {
petstore: {
output: {
// target: 'src/api/petstore.ts',
// target: 'src/api'
},
},
};
のように、ファイル名を指定する方法と、パスを指定する方法とがあります。
パスのみを指定した場合、OpenAPIドキュメントのタイトルがキャメルケースでファイル名となります。
output.client
HTTP Clientです。
angular, axios, axios-functions, react-query, svelte-query, vue-query, swr, zod, fetch
と、多彩です。
output.schemas
型定義のみをファイルとして生成可能です。
output: {
schemas: './src/api/schemas',
}
とすることで、サンプルコードでは以下画像のようにschemasが生成されます。
ファイル名で指定することで、一つにまとめることも出来ます。
output.mode
single, split, tags, tags-split
ファイルの生成モードです。
tags-splitを指定した場合は、tagによって生成されるディレクトリが分かれます。
導入方法
超簡単です。
$ yarn add -D orval
# 必要に応じ、msw, @faker-js
# mswファイルの準備に順序があるので注意
設定ファイルを作り、
npx orval
で自動生成コードをぶっ放します。
実際に関数を呼び出すコードが以下になります。
import { usePostsControllerFindAll } from '../api/orvalSampleApp'
export function PostsPage() {
const { data: posts } = usePostsControllerFindAll()
return (
<div className="max-w-4xl mx-auto px-4 py-8">
<h2 className='text-2xl font-bold mb-6'>Posts</h2>
<ul className="space-y-4">
{Array.isArray(posts) && posts.map((post) => (
<li
key={post.id}
className="bg-white overflow-hidden shadow-sm hover:shadow-md transition-shadow duration-200 rounded-lg border border-gray-100"
>
<div className='px-6 py-5'>
<h3 className='text-lg font-semibold text-gray-900 mb-2'>{post.title}</h3>
<div className='text-gray-600'>
<p>{post.content}</p>
</div>
</div>
</li>
))}
</ul>
</div>
)
}
そして動く。凄い。
おわりに
雨は止みまs
フロントエンドデベロッパーの村では、Orvalによって生成されたコードはAPIアクセスのみならず、コンポーネントの単体テストやStorybook表示にも用いられるようになったそうです。
良かったですね。
Twitterのフォローお願いします