OpenAPIを活用したスキーマの自動生成や型安全な開発について紹介
こんにちは、メディアエンジンの田中です!
弊社では、主にフロントエンド開発の効率化やバックエンドとフロントエンドのエンジニアのコミュニケーションの円滑化などを目的として、OpenAPIを活用しています。
この記事では、弊社でのOpenAPIの活用例などについて解説したいと思います。
OpenAPI Specificationとは
OpenAPI Specificationでは、特定の言語に依存せずにREST APIの仕様を記述するためのフォーマットなどが定義されています。
このOpenAPI Specificationを元にして様々なツールなどが開発されており、エコシステムが豊富であることなどが特徴です。
スキーマの管理について
バックエンドコードからのスキーマ自動生成
弊社では、バックエンドのAPIサーバをRailsまたはGo(+Echo)を使用して実装しています。
その際に、rswag
やswag
などを活用して、バックエンドのソースコードやテストコードなどからスキーマを自動生成させています。
rswag
rswag
はrspecを使って記述されたテストコードの内容をもとに自動でOpenAPIスキーマを生成してくれるgemです。
例えば、下記のようにしてRailsで実装したAPIに対するテストケースを記述することで、自動でスキーマが生成されます。
require 'request_helper'
require 'swagger_helper'
RSpec.describe 'Api::Users', type: :request do
path '/api/users/{id}' do
get 'returns a user' do
tags 'users'
produces 'application/json'
let(:user) { create(:user) }
let(:id) { user.id }
response '200', 'when authenticated' do
schema type: :array, items: { '$ref' => '#/components/schemas/User' }
run_test! do |response|
data = JSON.parse(response.body)
expect(data['id']).to eq(id)
end
end
end
end
end
共通で使用するレスポンスのスキーマ(上記の例ですと#/components/schemas/User
)はswagger_helper.rb
で定義しており、下記のようなコードになっております。
require 'rails_helper'
RSpec.configure do |config|
config.swagger_root = Rails.root.join('swagger').to_s
config.swagger_docs = {
'v1/swagger.yaml' => {
openapi: '3.0.1',
components: {
# 共通で使用するスキーマを定義
schemas: {
User: {
type: :object,
additionalProperties: false,
properties: {
id: { type: :integer },
name: { type: :string }
},
required: %w[id name]
}
},
info: {
title: 'API V1',
version: 'v1'
},
paths: {},
servers: [
{
url: 'https://{defaultHost}',
variables: {
defaultHost: {
default: 'www.example.com'
}
}
}
],
}
}
end
rswag
は、実際に返ってきたレスポンスの内容がスキーマの定義を満たしていない場合、テストを失敗させてくれるため、レスポンスの内容とスキーマの定義が食い違うことを防止することができて便利です。
swag
swag
は、Goのソースコードに記述されたコメントなどの内容を元に、スキーマを自動生成してくれます。
例えば、下記はEchoのハンドラにコメントを記述した例です。
// @ID getArticle
// @Summary 指定されたIDの記事を返却します
// @Tags articles
// @Security Bearer
// @Accept json
// @Produce json
// @Param id path int true "記事のID"
// @Success 200 {object} getArticleResponse
// @Failure 500 {object} echo.HTTPError
// @Router /api/articles/{id} [get]
func GetArticle(c echo.Context) error {
// ...
}
swag
は、このようなコメントや構造体などの定義を解析し、OpenAPIのスキーマ定義を自動で生成してくれます。
メリット・デメリットについて
これらのソースコードやテストコードからスキーマを生成するアプローチは、便利ではあるものの、チーム体制などによってはデメリットが出てくることも考えられそうです。
-
メリット
- ソースコードやテストコードを元にスキーマが生成されるため、スキーマがメンテナンスされないという事態に陥りにくい (はず)
-
デメリット
- フロントエンド開発の観点からすると、バックエンドのコードを触らないとスキーマを更新できないのが少々不便
そのため、チームの開発体制などによっては、ここで紹介したソースコードなどからスキーマを自動生成するアプローチをとるのではなく、直接スキーマを編集するようにした方がよい場合も考えられそうです。
その場合は、例えばStoplight Studioなどのツールの活用も検討するとよさそうです。
フロントエンド開発での活用
フロントエンド開発では、Vue.jsとTypeScriptを採用しています。
バックエンドとの間のやり取りはREST APIを介して行なっており、バックエンドで生成したOpenAPIのスキーマを様々な用途で活用しています。
ここでは、その一例について解説いたします。
型定義の自動生成
openapi-typescript
というパッケージを使用して、スキーマからTypeScriptの型定義を自動生成しています。
使い方としては、下記のようにしてOpenAPIのスキーマからTypeScriptの型定義を自動生成できます。
$ yarn openapi-typescript path/to/swagger.json \
-o types/openapi.ts \
--prettier-config .prettier
ここで生成された型は後述するスタブサーバやAPIクライアントを型安全にするために利用しています。
APIのスタブについて
フロントエンド開発をする上で、ちょっとしたUIの修正をしたい場合などは、APIのスタブサーバを用意できると便利です。
弊社では、元々この用途としてPrismを利用していたのですが、現在はmsw
へ移行しています。
ここではその経緯などについて解説いたします。
1. Prism
元々、Prismというツールを使用してAPIのスタブサーバを用意していました。
Prismは、OpenAPIのスキーマファイルを引数に指定するだけでスタブサーバを起動でき、手軽に利用できるのがとてもよいです。
$ prism mock -d path/to/swagger.json
また、Prismではx-faker
属性を宣言しておくことで、Faker.jsを使用したダミーデータの生成ができるのが便利だったりします。
ただし、このPrismを活用する中で、色々と課題も出てきたため、現在では次に解説するmswを活用しています。
2. msw
Prismが返却するデータをカスタマイズするためには、あらかじめexampleやx-faker
属性などを定義しておく必要があります。
しかし、先述した通り、弊社ではOpenAPIのスキーマをバックエンドのソースコードから自動生成するアプローチを採用しています。
そのため、フロントエンドエンジニアが開発時に使うテストデータを調整したいような場合でも、バックエンドのソースを修正する必要が出てきてしまいます。
これでは開発体験があまりよろしくないということで、より柔軟に利用ができるmswに移行することにしました。
また、mswを導入しておけば、テストコードやStorybookなどからもそれを活用できるなど様々な利点があります。
具体的な使用方法としては、openapi-typescript
で生成した型定義を活用するために、下記のようなヘルパを用意しています。
import assert from 'assert'
import { rest } from 'msw'
import type { RestRequest } from 'msw'
import type { Entry, ValueOf } from 'type-fest'
import type { paths, operations } from '@/types/openapi'
type MswMethod = keyof typeof rest
type Paths = keyof paths
type Operations = ValueOf<operations>
export function openapi<
TPath extends Paths,
TMethod extends keyof paths[TPath],
TOperation extends Operations = paths[TPath][TMethod] & Operations
>(path: TPath, method: TMethod, handler: (req: RestRequest) => Entry<TOperation['responses']>) {
const mswPath = openAPIPathToMswPath(path) // NOTE: OpenAPIとmswとではパスのフォーマットが異なるため、ここで正規化しています
return rest[method as MswMethod](mswPath, (req, res, ctx) => {
const [status, { schema }] = handler(req)
return res(ctx.json(schema), ctx.status(status as number))
})
}
function openAPIPathToMswPath(path: string): string {
return path.replaceAll(/{([^}]+)}/g, (_, $1) => {
return `:${$1}`
})
}
以下のようなイメージでハンドラーを定義できます。
const users = [
{ id: 1, name: 'foo' },
{ id: 2, name: 'bar' }
]
export const handlers = [
openapi('/users', 'get', () => {
return [
200,
{
schema: users
}
]
}),
openapi('/users/{id}', 'get', (req) => {
const { id } = req.params as paths['/users/{id}']['get']['parameters']['path']
const user = users.find((x) => x.id === id)
if (user == null) {
return [404, createError('NotFound')]
}
return [
200,
{
schema: user
}
]
}),
]
これにより、mswのハンドラーの定義をある程度型安全に行うことができます。(ただし、req.params
の部分でキャストをしており、ある程度妥協している状態です...😭 ここは改善していきたい)
APIクライアントの自動生成
OpenAPIのスキーマを元にAPIクライアントを生成したい場合は、おそらくopenapi-generator
が有名なのではないかと思います。
ただし、諸々の事情によりopenapi-generator
は採用できませんでした。
そのため、OpenAPIのスキーマからAPIクライアントを自動生成するスクリプトを自作しています。(内容としては、そこまで複雑なスクリプトではありません🙄)
イメージとしては、下記のような内容のコードを自動生成しています。
// openapi-typescriptで生成した型をimport
import type { operations } from '@/types/openapi'
interface APIClientOptions {
token: string
baseURL: string
}
export class APIClient {
constructor(readonly options: APIClientOptions) {}
['getUsers'](query: operations['getUsers']['parameters']['query']): Promise<operations['getUsers']['responses']['200']['schema']> {
return this.#request(`/api/users`, { params: query })
}
['getUser'](id: operations['getUser']['parameters']['path']['id']): Promise<operations['getUser']['responses']['200']['schema']> {
return this.#request(`/api/users/${id}`)
}
// ...省略...
}
APIクライアントを生成する際も、openapi-typescript
で自動生成した型を利用して、型安全にAPIを叩けるようにしています。
APIドキュメントのプレビュー
Railsなどのメジャーなバックエンドフレームワークでは、大抵の場合、OpenAPIで記述されたAPIドキュメントをプレビューする方法やライブラリなどが提供されていると思います。
しかし、フロントエンドエンジニアの観点からすると、APIドキュメントを閲覧するためにバックエンドの開発環境をセットアップするのは、やや大変な場合もあるかもしれません。
そういった場合は、RedocのCLIを活用すると、OpenAPIの定義さえあれば、バックエンド開発環境をセットアップせずともAPIドキュメントを閲覧できて便利です。
例えば、以下のようにしてAPIドキュメントを生成することができます。
$ npx redoc-cli build path/to/swagger.json
上記コマンドを実行するとHTMLが生成されるため、ブラウザで直接ドキュメントを閲覧できます。
$ open redoc-static.html
おわりに
以上、OpenAPIの活用例についての解説でした。
うまくいっている部分やそうでない部分など色々あるものの、少しずつ改善しています。
もしこの記事の内容が少しでも参考になりましたら幸いです。
少し宣伝が入りますが、弊社では現在エンジニアメンバーを募集中です。
弊社チームの紹介ページがあるので、興味がありましたらぜひ見に来てください!
Discussion