goaとopenapi-typescriptで楽々スキーマ駆動開発
はじめに
こんにちは、株式会社kivaのウチダです。DevOpsを効率化するのが好きです。
kivaでは、バックエンドにGo、フロントエンドにReactを採用しています。特に、FE・BE間の連携にはOpenAPIを共通言語として開発しており、開発速度の向上を狙っています。
このブログでは、実際にどのようにスキーマ駆動で開発しているのかを紹介します。
開発の流れ
全体イメージ
全体フローのイメージは下画像のようになります。BEで出力したOpenAPIをFEまで連れていきます。
API (Goa) の開発
バックエンドはGoでAPIサーバーを構築しており、Goaというフレームワークを利用しています。Goaでは、design
と呼ばれるAPIリクエスト・レスポンスの定義ファイルを記述することから開発が始まります(デザインファースト)。例えば、以下のような design
を記述し goagen
を実行すると、自動で構造体の定義やバリデーションの処理が実装され、同時にOpenAPIも生成されます。
package design
import (
. "goa.design/goa/v3/dsl"
)
// サービス定義
var _ = Service("concerts", func() {
Description("コンサートサービスは音楽コンサートのデータを管理します。")
Method("update", func() {
Description("IDで既存のコンサートを更新します。")
Payload(func() {
Extend(ConcertPayload)
Attribute("concertID", String, "更新するコンサートのID", func() {
Format(FormatUUID)
})
Required("concertID")
})
Result(Concert, "更新されたコンサート")
Error("not_found", ErrorResult, "コンサートが見つかりません")
HTTP(func() {
PUT("/concerts/{concertID}")
Response(StatusOK)
Response("not_found", StatusNotFound)
})
})
})
// データ型
var ConcertPayload = Type("ConcertPayload", func() {
Description("コンサートの作成/更新に必要なデータ")
Attribute("artist", String, "出演アーティスト/バンド", func() {
MinLength(1)
Example("The Beatles")
})
Attribute("date", String, "コンサート日付(YYYY-MM-DD)", func() {
Pattern(`^\d{4}-\d{2}-\d{2}$`)
Example("2024-01-01")
})
Attribute("venue", String, "コンサート会場", func() {
MinLength(1)
Example("The O2 Arena")
})
Attribute("price", Int, "チケット価格(USD)", func() {
Minimum(1)
Example(100)
})
})
var Concert = Type("Concert", func() {
Description("すべての詳細を含むコンサート")
Extend(ConcertPayload)
Attribute("id", String, "一意のコンサートID", func() {
Format(FormatUUID)
})
Required("id", "artist", "date", "venue", "price")
})
レポジトリの修正が完了したら、GitHubにプッシュしてOpenAPIを共有できる状態にします。
SPA (React) の開発
フロントエンドはSPAで開発しており、クライアントでAPIリクエストを行い、レスポンスを非同期レンダリングします。APIリクエストはGoaで生成したOpenAPIを用いて自動コード生成を行います。
まず、OpenAPIにアクセスできる環境を作ります。具体的には git submodule
を用いてバックエンドのリポジトリをフロントエンドのレポジトリに引き込みます。
[submodule "example-api"]
path = example-api
url = https://github.com/kiva-corp/example-api
branch = main
次に、openapi-typescriptを用いて型定義を自動生成します。なお、処理コマンドは Makefile
に定義し、スキーマの更新ごとに make update-types
を実行するだけで型定義の更新が完了します。
update-types:
make update-submodule
make gen-types
update-submodule:
git submodule update --init --remote example-api
gen-types:
bun openapi-typescript example-api/schema/goa/gen/conserts/gen/http/openapi3.yaml -o ./src/@types/v1.d.ts
生成された型定義を用いてAPIクライアントを構築するのですが、openapi-fetch を用いることで型定義からAPIクライアントを自動生成します。これにより、記述コード量を削減しつつ、スキーマの恩恵を受けながら開発が可能です。
import type { paths } from './src/@types/v1'
import createClient from 'openapi-fetch'
const { PUT } = createClient<paths>({
baseUrl: 'http://localhost:3333'
})
export async function updateConcert(concertID: string, artist: string) {
try {
const { data, error } = await PUT('/concerts/{concertID}', {
params: {
path: { concertID },
},
body: {
artist,
},
});
if (error) {
throw new Error(`API Error: ${error.status} - ${JSON.stringify(error)}`);
}
return data;
} catch (err) {
console.error('Update failed:', err);
throw err;
}
}
説明は以上です。
メリットとデメリット
前項の開発方法を運用していて実際に感じるメリットとデメリットを紹介します。総じて、型のないレポジトリを目にすると悲しくなります🥲
メリット
✅ APIテストが容易: ApidogやPostmanにOpenAPIをインポートして、APIのテストを簡単に実施可能。またkivaでは、OpenAPIを自動で共有フォルダにアップロードし、Apidogのスケジューリングインポートでメンバーに最新のスキーマを同期しています。
✅ 開発のミスを減らせる: 型安全性が向上し、リクエスト・レスポンスの不整合が発生しにくい。
✅ チーム開発がスムーズに: 共通スキーマを利用することで、フロントエンド・バックエンド間の意思疎通が容易に。
✅ コードの記述量が減る: 型定義の自動生成により、手書きのインターフェース定義が不要。
デメリット
⚠️ 学習コストがかかる: スキーマ駆動開発に慣れるまで、設定やツールの理解が必要。
番外編:スキーマ駆動に役立つツール
-
huma
- Goのスキーマ駆動フレームワークの一つ。個人的にはGoaの方が使い勝手が良いと思っています。humaの方が初見APIの見通しは良さげ。
-
oapi-codegen
- OpenAPIからGoの構造体やクライアントを自動生成。freeeやSentryなどOpenAPIを公開しているSaaSの型生成に利用しています。
-
typespec
- TypeScriptの使用感でOpenAPIを記述できるツール。既存のスキーマ駆動ではないレポジトリに導入しやすいです。
-
Orval
- OpenAPIからTypeScriptの型定義、APIクライアント生成、Zodバリデーション生成、キャッシュ管理を一括生成。フロントでやることがほぼ無くなります凄。
おわりに
いかがでしたでしょうか?
今回紹介した技術周辺でさらにおすすめな手法が多々あり、あとブログ10個は書けそうです。またそのうち公開するかも。
kivaでは、本質的な改善に着手できるようにDevOps環境の整備に積極的に取り組んでいます。スキーマ駆動開発を導入することで、開発の効率と品質が向上し、よりスムーズな開発フローを実現できます。
今後もより良い開発手法を模索し、継続的な改善を行っていきます。
Discussion