アルファテックブログ

OpenAPI TypeScriptで実現する型安全な開発

カバー

はじめに

フロントエンドとバックエンドがAPIを通じて連携する構成は、多くのWebアプリケーションにおいて一般的です。
そのような環境において、双方のやり取りに用いられるデータの型にズレが生じると、思わぬ不具合や保守性の低下を招くことがあります。

TypeScriptのような静的型付け言語は、コードの安全性や可読性を高める手段として広く活用されていますが、API通信の部分になると、手動での型定義やanyの多用により、その恩恵を活かしづらい場面も少なくありません。

たとえば、以下のような課題に直面したことはないでしょうか。

  • バックエンドのレスポンス定義を変更したことにフロントエンドが気づかずバグが発生する
  • フロントエンド側でanyを多用し、型の恩恵が受けられない
  • OpenAPIの仕様書は存在するが、実装と乖離しており信頼できない

この記事では、これらの課題を解決する手段としてOpenAPI TypeScript(公式サイト)を活用し、OpenAPI定義を中心に据えた、型安全なAPI連携の開発フローを構築する方法をご紹介します。

具体的にはNestJS(バックエンド)とTypeScriptベースのフロントエンドを例に解説しますが、構成そのものはフレームワーク非依存で利用可能です。
Webアプリケーション開発において型の整合性に課題を感じている方の一助となれば幸いです。

システムの全体構成

OpenAPI TypeScriptを活用した開発フローでは、OpenAPI仕様を軸に、バックエンド・フロントエンド双方で共通の型情報を共有することが重要なポイントとなります。

ここでは、その全体的な構成の流れを簡単に整理してみます。

  1. NestJSでOpenAPI仕様を生成 → openapi.yml
  2. openapi-typescriptで型定義を生成 → api-types.ts
  3. OpenAPI Fetchで型安全なクライアント生成
  4. フロントエンドで型を活用して開発

このようにOpenAPI仕様書を中心に据えることで、API設計と実装のズレを最小限に抑え、型の乖離による不具合を防ぐことができます。

ステップごとの実装

1. NestJSでOpenAPI仕様(openapi.yml)を生成する

まず、@nestjs/swaggerを使用して、OpenAPI形式の仕様書を自動生成します。
NestJSでOpenAPI形式のドキュメントを作成する手順については、過去の記事でも紹介しておりますのでこちらも合わせてご参照ください。

NestJSを用いたWebAPIサーバーの開発と、仕様書作成/APIテストの効率化

以下はエンティティクラスの定義例です。
@ApiPropertyデコレータを使用して、プロパティの情報を記述します。

[バックエンド] user.entity.ts
import { ApiProperty } from "@nestjs/swagger";
export class UserEntity {
@ApiProperty({
description: "ユーザID",
example: 1,
})
id: number;
@ApiProperty({
description: "ユーザの表示名",
example: "アルファ太郎",
})
name: string;
@ApiProperty({
description: "ユーザアカウント(ログインID)",
example: "alphataro",
})
account: string;
}

Controllerでも、@ApiResponseなどのデコレータを使用して、レスポンス情報を記述します。

[バックエンド] user.controller.ts
import { Controller, Get } from "@nestjs/common";
import { ApiOperation, ApiResponse } from "@nestjs/swagger";
import { UserEntity } from "./user.entity";
@Controller('users')
export class UserController {
constructor(private readonly userService: UserService) {}
@Get()
@ApiOperation({ summary: 'ユーザ一覧取得' })
@ApiResponse({
status: 200,
description: 'ユーザ一覧返却',
type: [UserEntity],
})
getAll() {
return this.userService.findAll();
}
@Get('/:id')
@ApiOperation({ summary: '特定ユーザ取得' })
@ApiResponse({
status: 200,
description: '指定したユーザを取得する',
type: UserEntity,
})
@ApiResponse({
status: 404,
description: 'ユーザが存在しない',
type: NotFoundError,
})
get(@Req() req: Request, @Param('id', ParseIntPipe) id: number) {
return this.userService.get(id);
}
}

main.tsにはドキュメント生成のコードを記載します。
バックエンドを起動すれば、OpenAPI形式のファイルが生成されます。

[バックエンド] main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { writeFileSync } from 'fs';
import * as path from 'path';
import { dump } from 'js-yaml';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const config = new DocumentBuilder()
.setTitle('API docs')
.setDescription('API仕様書です。')
.setVersion('1.0')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, document);
// ファイルに出力
const outputPath = path.resolve(process.cwd(), 'openapi.yml');
writeFileSync(outputPath, dump(document, {}));
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

2. OpenAPI TypeScriptで型定義を生成する

生成されたopenapi.ymlからTypeScriptの型を自動生成します。

Terminal window
npm install -D openapi-typescript
npx openapi-typescript ./openapi.yml -o ./src/api-types.ts

このapi-types.tsには、エンドポイントごとのパラメータやレスポンス型などが型定義として展開されます。

補足:型定義ファイルのカスタマイズ

出力される型定義ファイルに特定の変換や加工を加えたい場合は、専用のスクリプトファイルを用意することで柔軟に対応できます。

私が関わっているプロジェクトでは、以下を実現するためにスクリプトファイルを作成しました。

  • バイナリ形式のレスポンスをBlob型として扱う
  • 生成する型定義ファイルの先頭に、手動で変更してはいけない旨の警告と、型の再生成方法を追記する

3. フロントエンドで型安全なAPIクライアントを利用する

OpenAPI TypeScriptで生成した型を利用して、型安全なAPIクライアントを作成します。
OpenAPI Fetchを使用すれば、エンドポイントのURIを元に自動的に適切な型が適用されます。

[フロントエンド] webAPI.ts
import createClient, { Client } from "openapi-fetch";
import type { paths } from "../../api-types"; // ここで作成した型定義ファイルを指定します
export class WebAPI {
private static _instance: WebAPI | undefined;
private apiClient: Client<paths, `${string}/${string}`>;
private constructor() {
this.apiClient = createClient<paths>({
baseUrl: "http://localhost:3000",
});
}
public static get instance(){
if (!this._instance){
this._instance = new WebAPI();
}
return this._instance;
}
public async getUsers() {
const { data, error } = await this.apiClient.GET("/users");
if (!data) throw new Error(error);
return data; // ★バックエンドで定義した通りの型情報になっています
}
public async getUser(userId: number) {
const { data, error } = await this.apiClient.GET("/users/{id}", {
params: { path: { id: userId } },
});
if (!data) throw new Error(error);
return data; // ★バックエンドで定義した通りの型情報になっています
}
}

手動で型を定義することなく、簡単に適切な型が適用できました。

もちろんエディタ上でも型情報が表示されますので、開発体験も向上します。

Visual Studio Codeでの補完の表示

補足:URIの指定について

前述のコードでも示した通り、クライアントにはGET、POST、PUT、DELETEといったネイティブFetch APIをラップしたメソッドが用意されています。
これらのメソッドの第一引数にはURIを渡しますが、引数の型は文字列型ではなく、文字列リテラル型として定義されています。
そのため、タイプミスなどにより誤って存在しないAPIを指定してしまう可能性を排除できます。

■URIが補完されている様子
Visual Studio CodeでのURI補完の表示

■存在しないAPIを指定したときの様子
Visual Studio CodeでのURIのエラーの表示

型情報を直接参照したい場合

openapi-fetchを使用することで、APIクライアントの戻り値には自動で適切な型が適用されます。
ただ、場合によってはレスポンス型を手動で明示的に利用したい場面もあるかもしれません。

そのような場合は、OpenAPI TypeScriptで生成されたcomponents.schemasoperationsを直接参照することで、個別の型として利用できます。

import type { components } from '../../api-types';
// componentsから型を得る
export type UserDto = components['schemas']['UserEntity'];
// operationsから型を得る
export type UserDto2 = operations['UserController_get']['responses'][200]['content']['application/json; charset=utf-8'];

型の整合性チェックの実例

適切な型の管理によって得られる恩恵の一例をご紹介します。

たとえば、UserEntitynameプロパティをnicknameに変更してみます。

[バックエンド] user.entity.ts(抜粋)
@ApiProperty({
description: 'ニックネーム',
example: 'ニックネーム太郎'
})
nickname: string; // name → nickname に変更

この状態でopenapi.jsonを再生成し、さらにopenapi-typescriptで型を更新すると、フロントエンドの以下のようなコードにエラーが出るようになります。

Visual Studio Codeでの型エラーの表示

このように、バックエンドでの仕様変更が即座にエラーとして現れることで不整合を検出できます。

実際に導入してみて感じたメリット

私が関わっているプロジェクトでは、型安全を目的にこの仕組みを導入しましたが、それ以外の利点も感じられました。

バックエンドで記述した情報をJSDocとして活用できる

NestJSで@ApiProperty@ApiResponseといったデコレータに記述した情報は、OpenAPI TypeScriptを通じてJSDocコメントとして型定義に反映されます。

[バックエンド] user.entity.ts(抜粋)
export class UserEntity {
@ApiProperty({
description: 'ユーザアカウント(ログインID)',
example: 'alphataro'
})
account: string;

自動生成された型定義ファイル

api-types.ts(抜粋)
export interface components {
schemas: {
UserEntity: {
/**
* @description ユーザアカウント(ログインID)
* @example alphataro
*/
account: string;

これらの情報はエディタでも確認できるため、チームメンバーの理解の助けになりました。

Visual Studio Codeでの型情報のポップアップ

OpenAPI FetchのMiddlewareで通信処理を一元管理できる

openapi-fetchは、リクエストやレスポンスに対して共通処理を挿入できるMiddleware機能を備えています。

[フロントエンド] webAPI.ts(抜粋)
// Middlewareのインポートが必要
// import createClient, { Middleware, Client } from "openapi-fetch";
// Middlewareの定義(例として簡易的な処理を書いています)
const authMiddleware: Middleware = {
onRequest: async ({ request }) => {
const token = localStorage.getItem("access_token");
if (token) {
// 認証Tokenをセット
request.headers.set(
'Authorization',
`Bearer ${token}`,
);
}
return request;
},
onResponse: async ({ response }) => {
if (!response.ok) {
console.error("API Error:", response.status, response.statusText);
}
},
};
this.apiClient = createClient<paths>({
baseUrl: "http://localhost:3000",
});
// Middlewareの使用
this.apiClient.use(authMiddleware);

Middlewareの活用により、フロント側のAPIロジックがシンプルになり、テストしやすくなりました。

他にも以下のようなケースでの活用が考えられます。

  • 共通のリクエストログ出力
  • エラーレスポンスなどの共通のトースト表示
  • Content-TypeやAccept-Languageヘッダーの付与

まとめ

項目内容
OpenAPI TypeScriptOpenAPI仕様から自動でTypeScript型を生成
OpenAPI Fetch型定義と連携したクライアント生成が可能。Middlewareで共通処理も挿入できる
JSDoc連携@ApiProperty@ApiResponseなどの情報が型補完に活用され、開発体験向上に寄与
Middleware活用通信処理(認証・ロギングなど)の共通化と保守性の向上

今回の記事ではopenapi-fetchを中心に紹介しましたが、Reactを使っている場合は openapi-react-query のようなツールもあります。

おわりに

OpenAPI TypeScriptを活用することで、サーバーとクライアントのAPI定義を統一し、開発効率と保守性の高い、型安全なAPI連携を実現することができます。

この記事では、NestJSを用いたOpenAPI仕様の生成と、TypeScriptフロントエンドでの活用例をご紹介しました。
この構成は他のフレームワークやバックエンド技術にも十分適用可能です。

また、記事内では詳しく触れませんでしたが、実運用においてはopenapi.ymlの変更をトリガーとしてCI/CD上で型定義を自動生成する仕組みを導入することで、より確実に型の乖離を防ぐことができます。

紹介したアプローチは、API設計と実装の信頼性を高めたい現場において、有力な選択肢となり得ます。
スムーズに導入できる仕組みなので、興味のある方はぜひ試してみてください。

参考リンク


TOP
アルファロゴ 株式会社アルファシステムズは、ITサービス事業を展開しています。このブログでは、技術的な取り組みを紹介しています。X(旧Twitter)で更新通知をしています。