OrvalとSwaggoを用いた コードファーストでAPI設計駆動な爆速Webアプリ開発

本記事では、Goのハンドラー実装を元にSwaggoを使ってOpenAPIの仕様書(YAML)を自動生成し、さらにOrvalを利用してフロントエンド向けの型安全なAPIクライアントコードを生成する手法をご紹介します。開発者が関与する部分をできるだけ限定しつつ、バックエンドとフロントエンド間の仕様ズレを防止するための戦略もあわせて説明します。

著者

東 知哉

CTO

2025-4-16

2025-4-16

東 知哉

OrvalとSwaggoを用いた コードファーストでAPI設計駆動な爆速Webアプリ開発

#React

#TypeScript

#Go


はじめに

WebアプリのAPIを設計・実装する際、バックエンドとフロントエンドの間で設計や実装に齟齬が生じることはよくあります。例えば、事前にOpenAPIのyamlなどを記述して各エンドポイントの設計を行ったとしても、以下のような問題が発生することがあります。

  • バックエンド開発者が実装を設計どおりに作らず、フロントエンド開発者との実装に齟齬がでる。
  • そもそもの設計が間違っており、再度API設計書の修正からフロントエンドバックエンドそれぞれのコードを修正する必要がある。
  • フロントエンド側の実装でパラメータ名がわかりにくくタイポしており、原因解明に時間を取られる。

また、私たちディジョンのエンジニアは全員がバックエンド、フロントエンドどちらかの特定の領域に特化せずあらゆる領域に対して知識をもつフルスタックエンジニアとして開発をしています。そのため一人がとある機能をバックエンドからフロントエンドまで完結させて実装するケースが多く、そういった意味でも一人で特定のエンドポイントスキーマをTypeScriptでもgoでも実装するという手間の悪さは長らく課題と感じていました。

そんな私たちが、爆速且つ正確に実装することができるようになった方法を今回紹介します。

紹介する内容を実践することで

  • 楽に、爆速に開発ができる
  • フロントエンドとバックエンドとの実装の乖離をなくす
  • AI駆動開発を取り入れやすくする

これらの実現に大きく役立つと思います。ぜひ実践してみてください。

利用ライブラリの紹介

Swaggo

goのソースコードにアノテーションをつけることでOpenAPIのyamlファイルやJSONファイルを生成してくれるライブラリです。

https://github.com/swaggo/swag

私たちが採用しているEchoにも対応してくれています。

Orval

OpenAPIからコードを自動生成するためのライブラリです。詳しくはドキュメントや紹介記事を読んでいただきたいですが、Tanstack Queryを用いたAPIクライアントコードを生成してくれるのが私たちの技術スタックとマッチしていて非常に気に入っています。

https://orval.dev/

戦略と設計思想

ライブラリの紹介を見て思った方も多いと思いますが、今回やることはいわば「SwaggoからOpenAPIのyamlを生成し、そのyamlからTypeScriptコードを生成する」だけです。

しかしこの単純に見える方針の背景には十分に私たちの思想が含まれています。

OpenAPIをベースとした開発であればoapi-codegenのようなOpenAPIからGoコードを生成するライブラリもあり、それを使えばよりAPI設計ドリブンにはなります。

しかし、コードを書き且つ設計も行う私としては「YAMLファイルは書くのも読むのも難しい。コード上で簡単に設計を記述したい。」という気持ちがあったためコードファーストで設計をできる方法を探していました。まずはバックエンドにできるだけ簡素な形でソースコードを書けばあとはTypeScriptのコードが勝手に作られ、OpenAPIはあくまでも中間生成物、という設計と開発フローが効率的だと思いこの方針を取りました。

バックエンド実装: Go ハンドラーから OpenAPI YAML の生成

ここでは、詳細な実装は省略し、APIの基本情報やハンドラーの定義に注目します。

main.go

main.go には、OpenAPI の基本情報(タイトルやバージョン、ホストなど)をコメントとして記述しています。Swaggo はこれらのコメントから API ドキュメントを生成します。

package main
// @title ディジョンアプリ
// @version 1.0
// @license.name digeon-inc
// @BasePath /api
// @description APIの説明
// @host localhost:80
func main() {
// 省略
userHandler := handler.NewUserHandler()
// 省略
api := e.Group("/api")
api.GET("users/:user-id", userHandler.FindById)
defer e.Close()
// 省略
}

スキーマの定義

API のリクエストやレスポンスの型は、以下のように定義します。

ここで定義された構造体は、SwaggoのコメントによりOpenAPIに反映され、またフロントエンド側で利用するzodスキーマの元となります。

package schema
type UserFindByIDReq struct {
UserID string `param:"user-id"`
}
type UserRes struct {
UserID string `json:"userId" binding:"required"` // binding:"requiredをつけることでTypeScript生成時に非nullな値となる
Email string `json:"email" binding:"required"`
Name string `json:"name" binding:"required"`
UserType valueobject.UserType `json:"userType" binding:"required"`
Skills []string `json:"skills" binding:"required"`
} // @name UserRes

ハンドラーの定義

ハンドラー部分では、まず最低限の実装で OpenAPI 用のエンドポイントが作成されることを目的としています。

実際のビジネスロジックは省略していますが、設計段階では中身の実装よりもSwaggo用のコメントを付与してドキュメントとして整備することを目的とします。

アノテーションはフリーテキストながら、記載しているパラメータやレスポンスの指定方法は厳格なのでもし書き方が間違っていてもOpenAPI生成時にエラーを出してくれます。

package handler
type UserHandler struct {}
func NewUserHandler() UserHandler {
return UserHandler{}
}
// @Summary ユーザーの取得
// @Id findUserById
// @Tags user
// @Description ユーザーを取得します。
// @Param user-id path string true "ユーザーID"
// @Success 200 {object} schema.UserRes
// @Failure default {object} schema.ErrorRes
// @Router /users/{user-id} [get]
func (h UserHandler) FindById(c echo.Context) error {
// 設計フェーズ終了後実装する
return c.JSON(http.StatusOK, nil)
}

このように、最初の設計フェーズはOpenAPI作成に必要なコードのみを実装することで読み書き慣れたgoで設計することができます。

フロントエンドの連携: Orvalを用いた型安全なAPIクライアント生成

バックエンドで生成したOpenAPIの仕様書(swagger.yaml)を元に、Orvalを使用してTypeScriptのAPIクライアントコードを自動生成します。

これにより、フロントエンド側はTanstack Queryやaxios、さらにzodによるスキーマバリデーションを組み合わせた、型安全なAPI呼び出しが可能になります。

Orval設定ファイル

まず、Orval の設定ファイルで、生成元の OpenAPI ファイルのパスや出力先、axiosやTanstack Query用の設定を行います。


import { defineConfig } from "orval";
// 参照するOpenAPIファイルのパス
const inputPath = "../api/docs/swagger.yaml";
// 出力先のパス
const apiOutputPath = "./src/api";
const schemaOutputPath = "./src/types/api";
// axiosインスタンスのパス
const axiosPath = "./src/lib/apiClient.ts";
const axiosFunc = "customInstance";
export default defineConfig({
api: {
input: { target: inputPath },
output: {
mode: "tags-split",
target: apiOutputPath,
schemas: `${schemaOutputPath}/schema`,
client: "react-query",
clean: true,
mock: true,
override: {
// 参考:https://orval.dev/guides/custom-axios
mutator: {
path: axiosPath,
name: axiosFunc,
},
// API共通の設定
query: {
useSuspenseQuery: true, // SuspenseQueryの実装を生成する
useInfinite: true, // useInfiniteの実装を生成する
useInfiniteQueryParam: "page", // useInfiniteのクエリパラメータを設定する
},
},
},
},
zod: {
input: { target: inputPath },
output: {
mode: "tags-split",
client: "zod",
target: apiOutputPath,
fileExtension: ".zod.ts",
},
hooks: {
afterAllFilesWrite: "npm run fix",
},
},
});

axiosのカスタマイズと設定

Orvalによって生成される呼び出しメソッドは、共通のaxiosインスタンスを利用するため、カスタムのaxios設定を行います。

認証トークンの付与やエラーハンドリングなどもここで定義可能です。

export const api = Axios.create({
baseURL: env.API_URL,
});
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response == null) {
return Promise.reject(new Error("レスポンスがありません。"));
}
const statusCode: number = error.response.status;
const errorCode: ErrorCode = error.response.data?.code;
const message = getErrorMessage(statusCode, errorCode);
return Promise.reject(new Error(message));
},
);
// Orvalにカスタムのaxiosインスタンスを渡すための関数
// 参考:https://orval.dev/guides/custom-axios
export const customInstance = <T>(config: AxiosRequestConfig): Promise<T> => {
const source = Axios.CancelToken.source();
const promise = api({
...config,
cancelToken: source.token,
}).then(({ data }) => data);
return promise;
};

Orval自動生成コードの例

Orvalを実行することで以下のようなコードが生成されます。これはあくまでも一部抜粋で実際はもっと大量のコードが作成されます。設定しだいではモックの実装やInfinite, Suspense版のクエリも生成してくれます。

/**
* ユーザーを取得します。
* @summary ユーザーの取得
*/
export const findUserById = (userId: string, signal?: AbortSignal) => {
return customInstance<UserRes>({
url: `/users/${userId}`,
method: "GET",
signal,
});
};
export const getFindUserByIdQueryKey = (userId: string) => {
return [`/users/${userId}`] as const;
};
export type FindUserByIdQueryResult = NonNullable<
Awaited<ReturnType<typeof findUserById>>
>;
export type FindUserByIdQueryError = ErrorRes;
export function useFindUserById<
TData = Awaited<ReturnType<typeof findUserById>>,
TError = ErrorRes,
>(
userId: string,
options: {
query: Partial<
UseQueryOptions<
Awaited<ReturnType<typeof findUserById>>,
TError,
TData
>
> &
Pick<
DefinedInitialDataOptions<
Awaited<ReturnType<typeof findUserById>>,
TError,
Awaited<ReturnType<typeof findUserById>>
>,
"initialData"
>;
},
): DefinedUseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData>;
};
/**
* @summary ユーザーの取得
*/
export function useFindUserById<
TData = Awaited<ReturnType<typeof findUserById>>,
TError = ErrorRes,
>(
userId: string,
options?: {
query?: Partial<
UseQueryOptions<
Awaited<ReturnType<typeof findUserById>>,
TError,
TData
>
>;
},
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData> } {
const queryOptions = getFindUserByIdQueryOptions(userId, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData>;
};
query.queryKey = queryOptions.queryKey;
return query;
}

そして、フロントエンドでの使用例は次のようになります。Tanstack Routerを用いてルーティングした先で、パラメータを用いてOrvalの関数を呼び出し、型推論が効いた状態でデータを扱うことができます。

/**
* ユーザ詳細画面
* @returns
*/
export function DetailUser() {
const { userId } = useParams({
from: "/_authenticated/users/$userId/detail",
});
// Orvalが生成した関数をimportして呼ぶだけ!!
const { data } = useFindUserById(userId);
return (
<div className="w-full p-6 space-y-4">
<Card noPadding header="ユーザー情報">
<DetailText key="name" label="名前" value={data.name} />
<DetailText
key="email"
label="メールアドレス"
value={data.email}
/>
<DetailText
key="skills"
label="スキル"
value={
<div className="flex flex-wrap space-x-2">
{data.skills.map((s) => (
<Badge key={s} className="text-sm">
{s}
</Badge>
))}
</div>
}
/>
<ButtonToLink
key={"edit"}
variant="secondary"
to={{
to: "/users/$userId/edit",
params: { userId },
state: true,
}}
className="w-full md:w-fit"
>
編集する
</ButtonToLink>
</Card>
</div>
);
}

このように、API の呼び出しに関しては全く実装を記述することなく、Orvalが自動生成したコードをそのまま活用できる点が非常に魅力的です。

自動化のための仕組み

バックエンドのコードコメントからOpenAPIのYAMLを生成し、さらにOrvalでTypeScriptコードを生成する一連の流れは以下のような簡単なシェルスクリプトでまとめることができます。これにより、ローカル開発時および CI 上で一貫した生成処理を実行でき、実装のズレを防止できます。

#!/bin/bash
# 環境構築スクリプト
current_dir=$(pwd)
cd "$current_dir/api" && swag init
cd "$current_dir/app" && npm run orval

実装ズレチェックのGithub Actionsの例

jobs:
validate-sync:
name: check-sync
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.23"
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Install Dependencies
run: |
cd app
npm ci
cd ../api
go install github.com/swaggo/swag/cmd/swag@latest
- name: Generate Swagger YAML
run: |
sh create_api.sh
- name: Check for Uncommitted Changes
run: |
git diff --exit-code || (echo "Uncommitted changes detected. Please regenerate and commit swagger.yaml and/or generated code." && exit 1)

この戦略により、API 仕様の自動生成と連携した統一開発環境が実現し、設計変更や仕様のズレによるトラブルを未然に防ぐことができます。

また、Swaggo -> OpenAPI -> Orvalの流れを一つのコマンドにして利用することで、仮にAPI設計が途中で変わってもその変更が即座にフロントエンドのコード側に反映されるため修正の手間を旧来の方式に比べて大きく減らすことができます。

おわりに

現状Webアプリ開発の領域の多くはまだ人の手が介入する状況ですが、ここ数ヶ月のAI駆動開発技術の発達でどんどんソースコード開発は自動化されていくでしょう。

一方で、まだしばらく人が仕様を明確に定義し、各層での整合性を保つことが必須です。

また、DevinやGithub CopilotのようなAI補助ツールがどれだけ発達しても、今回のアプローチは必要と考えています。特定のルールでコードを自動生成してくれる領域が多ければ多いほど人にとってもAIにとっても精度の高く実装できる基盤を作ることができます。


今は、Goだけではなく、バックエンドもTypeScriptで実装するアプリケーションの実装パターンを試しているところです。フレームワークとして利用しているHonoは標準機能でSwaggoと似たようなことをzodスキーマベースで行えるためこちらも大きく期待をしています。

本記事が有用だと感じたらぜひ、皆さんのプロジェクトに取り入れて、開発体験の向上に役立ててください。

ディジョンは現在エンジニアの採用を積極的に行っています。AI駆動開発を実現するためにはまだまだ改善していきたいところが数多くあります。興味のある方はぜひご応募ください。

Share


xのアイコンfacebookのアイコンこのエントリーをはてなブックマークに追加

Author


著者

東 知哉

CTO

神戸大学大学院の修士課程を修了し、ヤフー株式会社でエンジニアとして勤務。その後、ディジョンに入社。


共に働く仲間を募集しています

Digeonは意欲のある方を積極的に採用しています。
神戸発のAIベンチャーでAIの社会実装を一緒に進めませんか?

採用ページはこちら
logo
Engineering Portal
ディジョンのエンジニア情報ポータルサイト
©株式会社Digeon All Rights Reserved.