openapi2aspida を用いた快適スキーマ駆動開発
この記事の目的
openApi2aspidaを使用し、型を自動生成した開発が快適だったため便利だった点などを紹介していきます。
はじめに
フロントエンドの開発において、バックエンドからのレスポンスの内容をtypes
フォルダに書くことが時々あると思います。
例えば、ユーザの情報について下記のようなレスポンスが返ってくるときに、
// types/user.ts
type User = {
id: stirng;
name: string;
role: 'admin' | 'free';
};
上記のように型を定義して、その型を元にコンポーネントで使用したりすると思います。
import { User } from '@/types/user';
export const UserInfo = (props: User) => {
// ....
};
openApi2aspidaを使用すると、この型を定義する手順が不要になり、フロントエンド実装者のプロパティ名の記述ミスなどを未然に防ぐことができます。
また、openApi2aspida では、OpenAPI のcomponents
(FYI: https://swagger.io/docs/specification/components/ )に型情報を集めることで、下記のような@types
フォルダが生成されます。
├── $api.ts
├── @types
│ └── index.ts
├── comments
│ ├── $api.ts
│ └── index.ts
└── posts
├── $api.ts
├── _id@string
│ └── index.ts
└── index.ts
@types
フォルダの中身は下記のようになっており、
// 自動生成される
/* eslint-disable */
export type Post = {
id: number;
title: string;
author: string;
};
export type Comment = {
id: number;
body: string;
postId: number;
};
この自動生成される型を用いてフロントエンドの開発を行うことで、自分で型を定義することなく開発を行うことができます。
また、バックエンドのレスポンスなどに変更があった際も、OpenAPI で書かれた定義を変更し、openapi2aspida を用いて型を再生成すれば自動で@types
フォルダの中身も更新されるため、フロントエンドとバックエンドのレスポンス内容の乖離を防ぐことができます。
aspida について
そもそも aspida がどういうものかについては、作者の方がわかりやすい記事を作成してくれており、基本的にこちらの記事を見ると使い方やライブラリの良さがわかると思います。
openapi2aspidaについても README.md の内容そのままですが、
npx openapi2aspida -i https://petstore.swagger.io/v2/swagger.json
のようにopenapi2aspida を使用すると api の定義から aspida 用の型定義を作成してくれます。
便利だった点
生成された型の安心感
api からのレスポンスを自分で型定義する必要がなく、OpenAPI に書かれた定義を元に実装を行うことができるので、OpenAPI の定義が間違っていない限りフロントエンド側ではプロパティ名などを間違えることがなくロジックの実装に集中することができます。
こちらの記事でも下記の点が問題として挙げられていますが、
API 側とフロント側での型定義は連携が取れていないので、現在はそれぞれに型定義ファイルが存在しています。今後 Open API を使うなどして、二重管理になっている型定義を共通化していきたいと考えています。
バックエンド側とフロントエンド側で一度 OpenAPI で仕様を決めておくことで、
API 側とフロントエンド側の定義を OpenAPI で一括管理することで、スキーマ駆動に開発を進めることができ、開発速度の向上を実感できました。
テストのモックレスポンスを書かなくて済む
テストにおいて api からのレスポンスを記述するというのはあると思います。
データを表示したり使用するコンポーネントをテストする場合にはレスポンスを定義することはありますし、MSWを使用する場合でも、
rest.get('/user', (req, res, ctx) => {
return res(
ctx.json({
firstName: 'John',
age: 38,
})
);
});
上記のように json の中身を記述することがあると思います。このレスポンスに OpenAPI の example(https://swagger.io/docs/specification/adding-examples/ )を使用することで、レスポンスを自分で定義する必要がなくなります。
そのためには OpenAPI を json で書くのが好ましく、json を import して、下記のように example の値を使用できます。
import schema from '../schema/api.json';
...
rest.get('/posts', (req, res, ctx) => {
return res(ctx.status(200), ctx.json(schema.components.schemas.Post['x-examples']['example-3']));
}),
...
このままだと、schema.schema....
の部分が長いので、components.ts
などのファイルに切り出して、使用するのが良いでしょう。
import schema from '../schema/api.json';
const components = {
Posts: [
schema.components.schemas.Post['x-examples']['example-1'],
schema.components.schemas.Post['x-examples']['example-2'],
schema.components.schemas.Post['x-examples']['example-3'],
],
Post: schema.components.schemas.Post['x-examples']['example-1'],
};
export default components;
この components を使用して、handler を定義すると下記のようになります。
// handlers.ts
import { rest } from 'msw';
import { Post } from '../api/@types';
import components from './components';
export const handlers = [
// 記事一覧取得
rest.get('/posts', (req, res, ctx) => {
return res(ctx.status(200), ctx.json(components.Posts));
}),
このような感じですね。
この方法は、
で紹介されており、かなり参考にさせていただきました。MSW を使わずに、Jest.mock を使用した場合にも下記のように components を使用することで個別にレスポンスを記述する必要はありません。
jest.mock('@/utils/api.ts', () => ({
$axios: {
$get: jest.fn(() => Promise.resolve(components.Posts)),
},
}));
まとめ
openapi2aspidaを使用して快適なフロントエンド開発を行う方法についての説明でした。GraphQLを用いた開発の場合には、
などを使用するとより型が厳密な開発ができると思いますが、リソースなどの影響で、バックエンドでGraphQLを使用できない場合などに、今回紹介したopenapi2aspidaを用いた開発でスキーマ駆動で快適な開発が行えるかと思います。以上、openapi2aspidaを使用したスキーマ駆動開発の良かった点などの感想でした。
コード
- source: nextjs-app-with-openapi
参考文献
Discussion