【OpenAPI】TypeScript+ReactとASP.NET Web API間でデータをやり取りする
TypeScriptアドベントカレンダー24日目の記事です。
残念ながらTypeScriptの要素は2割くらいしかありません(すまん)。
この記事で話すこと
TS + Reactで作ったフロントエンドとASP.NETで作ったバックエンド間のデータのやり取りをする例を紹介します。
ありそうな例で言うと、「C#で書かれた既存のWeb APIサーバーを使って新たにWebアプリを作りたいけど、フロントサイドの技術者がTS/JSしか使えないとき」みたいな感じでしょうか。稀によくある(無い)。
完成形
↓のようにボタン押下時にリクエストを送り、戻ってきたデータをそのまま表示します。
構成は次の通りシンプルです1。
実際のプログラムなどは↓のレポジトリで公開しています。
最終的なディレクトリ構成
root
┣ /front
┃ ┗ /openAPI
┗ /webAPI
今回は特にDockerも使うことなく、Windows11上にプロジェクトを作りました。自動生成されるインターフェイスを含むフォルダを/openAPI
と名付けています。
今回適当なディレクトリ構成にしていますが、Gitで管理されている場合はopenAPI
フォルダ自体をsubmoduleとして導入することになるんじゃないかと思います。モノレポにして運用する方法もあるかと思いますが、その辺りはまあ皆さんのチームの状況を踏まえて決めればよいと思います(適当)(要するにケースバイケース)。
手順
Web APIのプロジェクトを作成
powershellでもコマンドプロンプトでも何でも良いのですが、dotnet
コマンドを打てるターミナルを用意してください。次のコマンドで localhost:1234/weatherForecast のようなエンドポイントを持つAPIを作れます。出力先フォルダ名はwebapiとします。
dotnet new webapi -o webapi
生成されたProgram.cs
を見てみると、次のようにweatherForecast
APIが自動生成されているのが分かります。今回はこのエンドポイントをそのまま使います。
app.MapGet("/weatherforecast", () =>
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
})
.WithName("GetWeatherForecast")
.WithOpenApi();
...省略
// NOTE: ご丁寧にrecordで定義してくれている
record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
⚠️ ここでNugetの参照先がMicrosoft Visual Studio Offline Packages になっていると、パッケージが見つからない旨のエラーが出ます。エラーが出た場合は、onlineのパッケージをグローバルで参照するように次のコマンドで変更してください。2
dotnet nuget add source https://api.nuget.org/v3/index.json -n nuget.org
OpenAPIスキーマファイルの生成
.NETのWeb APIプロジェクトでは、コードやスキーマファイルの自動生成用ライブラリとしてSwashbuckle
やNSwag
が主に用いられています。Miscrosoftの公式ドキュメントでもこれら二つが取り上げられています。
今回は近年主流になっているNSwag
を使ってスキーマファイルを生成します。詳細は上記のドキュメントを読んでください。
まずNSwag
を追加します。
dotnet add package NSwag.AspNetCore
次の呪文を追加します。
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddOpenApiDocument(); // ここを追加
... 省略
if (app.Environment.IsDevelopment())
{
// Add OpenAPI 3.0 document serving middleware
// Available at: http://localhost:<port>/swagger/v1/swagger.json
app.UseOpenApi(); // ここを追加
// Add web UIs to interact with the document
// Available at: http://localhost:<port>/swagger
app.UseSwaggerUi(); // ここを追加
}
サーバーのポート番号を指定するにはProperties/launchi.json
のapplicationUrlを編集します。今回は適当に5200番にしておきました。
"applicationUrl": "http://localhost:5200",
この状態で、dotnet run
コマンドでサーバーを起動します。localhost:5200/swagger
に移ると、↓のようにSwagger UIが表示されると思います。
上記で上手くいかない場合は↓の記事を参照してください。
先ほどの図中に表示されていた、「/swagger/v1/swagger.json」の部分をクリックすると、自動生成されたjsonファイル(OpenAPIのスキーマファイル)の中身を見ることができます。そのjsonファイルを保存しておいてください。
スキーマファイルの自動生成について(WIP)
.NET Core Web APIのプロジェクトでは、NSwag.MSBuild
を使うことでプロジェクトのビルド時などにOpenAPIのスキーマファイルを自動生成することが可能です。下記の記事でも説明されているような方法を取ることで、ローカル側にスキーマファイルを自動生成することができます。
NSwag.MSBuild
のドキュメントはこちら。
しかしながら2024/2月現在、NSwag.MSBuildの最新バージョン14.0.3
では.NET 8.0環境(恐らく7.0でも)でCLIに不具合が生じており、この機能を使うことができません(少なくとも私の環境では)。恐らく14.1
以上のバージョンからは修正されると思いますので、修正されたらこの記事でも対応しようと思います。
詳細は↓のissueに上がっています。
Reactアプリの作成
Vite
を使ってちゃちゃっと動くアプリを作成します。npm
コマンドが動く状態で、以下のコマンドでReactアプリを作成・動作確認します。今回フォルダ名はfront
としました。
npm create vite@latest
cd front
npm install
npm run dev
アプリの実行先のURLに飛ぶと次のような画面が表示されます。
確認出来たらsrc
フォルダ内のApp.tsx
を↓の様にまっさらにしておきます。
import "./App.css";
function App() {
return <></>;
}
export default App;
OpenAPI Generatorでプログラムを自動生成
今回は数あるコードジェネレータの中でも老舗のOpenAPI Generatorを使います。次のコマンドでDevDependencyとしてインストールします。
npm install @openapitools/openapi-generator-cli -D
front
(Reactのフォルダ)内のsrc
フォルダ内にopenapi
フォルダを作成し、フォルダ内に移動します。
cd ./front/src && mkdir openapi && cd openapi
ここはどのような方法でも構いませんが、先ほどダウンロードしておいたswagger.json
を、作成したopenapi
フォルダに移動させておきます。
mv path/to/swagger.json ./
openapi
フォルダ内でswagger.json
を元にTypeScript + axiosに見合ったAPIリクエスト用のプログラムを生成させます。次のコマンドを実行してけれ。
npx @openapitools/openapi-generator-cli generate -i swagger.json -g typescript-axios
上手くいけば幾つかファイルが生成されると思います。ひとまずTypeScriptのファイルだけが重要なので他のファイルは無視してください。生成されたプログラムはChatGPTに読ませれば使い方とか全て説明してくれると思うので、この記事では説明しません。
リクエスト送ってみるンゴ!
api.ts
で定義されている次の関数を使ってリクエストを送ってみます。
export const DefaultApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
const localVarFp = DefaultApiFp(configuration)
return {
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getWeatherForecast(options?: any): AxiosPromise<Array<WeatherForecast>> {
return localVarFp.getWeatherForecast(options).then((request) => request(axios, basePath));
},
};
};
この関数を使って↓みたいな書き方でレスポンスを得られる事が分かります。
const response = DefaultApiFactory().getWeatherForecast()
あとAPIサーバー側でCORSの設定も忘れていたので以下の様に追加しました。アプリのポート番号
の部分を実際のポート番号に書き換えてください。
// CORSの設定を追加
var MyAllowSpecificOrigins = "_myAllowSpecificOrigins";
builder.Services.AddCors(options =>
{
options.AddPolicy(name: MyAllowSpecificOrigins,
policy =>
{
policy.WithOrigins("http://localhost:Reactアプリのポート番号").AllowAnyHeader().AllowAnyMethod();
});
});
... 略
app.UseHttpsRedirection();
var summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
// ここも必要
app.UseCors(MyAllowSpecificOrigins);
ということで、最終的に以下のようなコードでリクエストを送信・データの表示を行えるようになりました🎉
// 最終的なフロント側のコード
import { useState } from "react";
import "./App.css";
import { DefaultApiFactory, WeatherForecast } from "./../openapi/api";
function App() {
const [forecasts, setForecasts] = useState<WeatherForecast[]>([]);
const fetchData = async () => {
try {
const response = await DefaultApiFactory().getWeatherForecast();
setForecasts(response.data);
} catch (error) {
console.log(`Featching data failed. ${error}`);
}
};
return (
<>
<button onClick={() => fetchData()}>リクエストするンゴ~</button>
<table>
<thead>
<tr>
<th>Date</th>
<th>Temperature</th>
<th>TemperatureF</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
{forecasts.map((forecast, index) => {
// なぜかforecast.Date等がundefinedになってしまう謎
// 仕方なく各値を取り出している
const [date, tempC, tempF, summary] = Object.values(forecast);
return (
<tr key={index}>
<td>{date}</td>
<td>{tempC}</td>
<td>{tempF}</td>
<td>{summary}</td>
</tr>
);
})}
</tbody>
</table>
</>
);
}
export default App;
forecast.data
等のプロパティの値がundefinedになってしまう謎に出会ってしまったため、仕方なくObject.values
メソッドでお茶を濁しました。本当に分からん。教えてえろい人。
とりまこれでうまく動きました。めでたし。
まとめなんてない
- 巨人(OpenAPI Generator)の型に乗って行け
- APIサーバー側でトークンベースのユーザー認証を付けた時とか、送受信するデータの検証を行うようなときにどんなプログラムが生成されるのか、わたし、気になります!