♻️
rswag と OpenAPI TypeScript で型安全を向上する
Rails × TypeScript の開発において、テストファイルから自動で型定義を生成する方法をまとめました。
手動での型定義を避けることで、開発の安全性を向上させることが可能になります。
流れ
- rswag で OpenAPI を生成する
- OpenAPI TypeScript で型定義を自動生成する
- 生成された型を使って API クライアントを型安全にする
1. rswag で OpenAPI を生成する
インストール
Gemfile
gem 'rswag'
$ bundle install
$ rails g rswag:install
リクエストスペックを書く
rswag ではリクエストスペックを元に OpenAPI を生成します。
ここでは /api/articles の GET/POST をカバーするシンプルな例を用意します。
spec/requests/api/articles_spec.rb
require 'swagger_helper'
RSpec.describe 'Articles API', type: :request do
let!(:articles) { create_list(:article, 2) }
path '/api/articles' do
get '記事一覧を取得する' do
tags 'Articles'
produces 'application/json'
response '200', '成功' do
schema type: :array, items: {
type: :object,
properties: {
id: { type: :integer },
title: { type: :string },
body: { type: :string }
},
required: %w[id title body]
}
run_test! do |response|
data = JSON.parse(response.body)
expect(data.size).to eq(articles.size)
end
end
end
post '記事を作成する' do
tags 'Articles'
consumes 'application/json'
parameter name: :article, in: :body, required: true, schema: {
type: :object,
properties: {
article: {
type: :object,
properties: {
title: { type: :string },
body: { type: :string }
},
required: %w[title body]
}
},
required: %w[article]
}
let(:article) { { article: { title: 'Hello', body: 'world' } } }
response '201', '作成に成功' do
schema type: :object,
properties: {
id: { type: :integer },
title: { type: :string },
body: { type: :string }
},
required: %w[id title body]
run_test! do |response|
expect(response.status).to eq(201)
end
end
end
end
end
OpenAPI の生成
スペックを書いたら、rswag:specs:swaggerize を叩くことで swagger/v1/swagger.yaml が更新されます。
$ bundle exec rake rswag:specs:swaggerize
# swagger/v1/swagger.yaml に OpenAPI 3.0.1 の定義が出力される
この YAML が API の仕様書兼、TypeScript へ型を引き継ぐためのソースになります。
2. OpenAPI TypeScript で型定義を自動生成する
OpenAPI の仕様が手に入ったら、openapi-typescript で TypeScript 型を生成します。
インストール
$ npm install --save-dev openapi-typescript typescript
package.json にスクリプトを追加しておくと便利です。
package.json
{
"scripts": {
"openapi:generate": "openapi-typescript swagger/v1/swagger.yaml -o src/types/generated/openapi.d.ts"
}
}
型生成コマンド
$ npm run openapi:generate
# src/types/generated/openapi.d.ts が生成される
生成されたファイルは巨大ですが、paths キー以下に API ごとのレスポンスやリクエストの型が定義されています。今回の /api/articles の GET なら次のような形になります。
src/types/generated/openapi.d.ts
export interface paths {
"/api/articles": {
get: {
responses: {
200: {
content: {
"application/json": {
id: number;
title: string;
body: string;
}[];
};
};
};
};
post: {
requestBody: {
content: {
"application/json": {
article: {
title: string;
body: string;
};
};
};
};
responses: {
201: {
content: {
"application/json": {
id: number;
title: string;
body: string;
};
};
};
};
};
};
}
3. 生成された型を使って API クライアントを型安全にする
3. 生成された型を使って API クライアントを型安全にする
型が用意できたので、フロントエンド側からはそれをインポートして使うだけです。fetchArticles.ts を例にします。
src/api/fetchArticles.ts
import type { paths } from "../types/generated/openapi";
type ArticlesIndexResponse =
paths["/api/articles"]["get"]["responses"]["200"]["content"]["application/json"];
export const fetchArticles = async (): Promise<ArticlesIndexResponse> => {
const response = await fetch("/api/articles");
if (!response.ok) {
throw new Error("Failed to fetch articles");
}
const data = (await response.json()) as ArticlesIndexResponse;
return data;
};
POST のリクエストボディも同様に扱えます。
src/api/createArticle.ts
import type { paths } from "../types/generated/openapi";
type CreateArticleRequest =
paths["/api/articles"]["post"]["requestBody"]["content"]["application/json"];
type CreateArticleResponse =
paths["/api/articles"]["post"]["responses"]["201"]["content"]["application/json"];
export const createArticle = async (
payload: CreateArticleRequest
): Promise<CreateArticleResponse> => {
const response = await fetch("/api/articles", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error("Failed to create article");
}
const data = (await response.json()) as CreateArticleResponse;
return data;
};
生成された OpenAPI 型をそのまま使うメリットは、バックエンドの変更がそのままコンパイルエラーになって表面化することです。
例えば title が headline にリネームされれば、openapi:generate を再実行した時点で fetchArticles や createArticle で型エラーが発生し、見逃しを防げます。
まとめ
- rswag を使うと、RSpec で書いたリクエストテストから OpenAPI 仕様を生成できる
- openapi-typescript でその仕様から TypeScript の型を自動生成できる
- 生成された型を使用することで、BE と FE の仕様差異を検知しやすくなる
Discussion