.NET AspireでSPA構成のフロントエンドとバックエンドを同時に起動する

こんにちは。

テックリードのTerukiです。

Oh my teethではバックエンドにASP.NET Core、フロントエンドにReactを使うシステムがほとんどとなっているのですが、開発体験を上げるために.NET Aspireを使ってみることにしたのでその記録です。

# .NET Aspire

https://learn.microsoft.com/en-us/dotnet/aspire/get-started/aspire-overview

開発時の複数プロジェクトを良い感じにオーケストレーションしてくれて開発体験が上がるやつという雑な理解でいます。

いろいろな機能がありますが、今回はタイトルの内容に絞ってやってみようと思います。

# モチベーション

フロントエンドとバックエンドが完全に独立したプロジェクトになっているので、バックエンドをVisual StudioやRiderで起動した後に手動で `pnpm run dev` をターミナルで実行してデバッグするといったことを当たり前のようにやっています。

これには単に面倒という面もありますが、複数プロジェクトを並列して開発している時にはプロジェクトAのバックエンドがプロジェクトBのフロントエンドに繋がっていてエラーが大量に出るみたいなことがたまに発生します。

で、プロジェクトAのフロントエンドはポート番号がViteのデフォルトの5173ではなく5174で起動しているわけです。

複数のViteを動かすとこういうことが起きがちです。

これらを一気に改善できたら非常に良い感じです。

# 導入

実際に既存のプロジェクトに導入したのでソースコードをフル公開できるわけではないですが、部分的にでもコードを出せたらと思います。

AppHostを既存のソリューションに追加した後Program.csです。

```cs
var builder = DistributedApplication.CreateBuilder(args);

var frontend = builder.AddViteApp("frontend", "../frontend", "pnpm")
    .WithPnpmPackageInstallation();

builder.AddProject<Projects.Backend>("backend")
    .WithReference(frontend);

builder.Build().Run();
```

AddViteAppをできるようにするためにAppHostプロジェクトにNuGetパッケージを追加します。

```
CommunityToolkit.Aspire.Hosting.NodeJS.Extensions
```

slnファイルがあるディレクトリに、frontendという名前のディレクトリがあり、そこにpackage.jsonやらのフロントエンドのファイルがある構成になっています。

Oh my teethではpnpmを使っていますがnpmだったりyarnだったりを指定できます。

`WithPnpmPackageInstallation()`を指定することで、起動時に `pnpm install` を勝手にやってくれます。これも地味に便利です。

## フロントエンド

viteがデフォルトでは5173番のポートで起動しますが、Aspireは動的にエフェメラルポートを割り当てます。

ただ、そのポート番号をちゃんとviteに教えてあげないと依然として5173番で起動してしまうので、ポート番号を指定します。

```ts
// https://vite.dev/config/
export default defineConfig({
  // 略
  server: {
    port: process.env.PORT ? parseInt(process.env.PORT, 10) : 5173,
    // 略
  },
});
```

環境変数`PORT`にポート番号を入れてくれるので、それがあれば指定する感じです。

package.jsonからコマンドラインでポート番号を渡す方法もありますが、上記のほうが分かりやすいかなと個人的に思います。

## バックエンド

バックエンドについては、APIリクエストはバックエンドで処理してそれ以外のリクエストはすべてviteのサーバに流します。

公式の便利なパッケージがあるのでそれを使います。

```
Microsoft.AspNetCore.SpaServices.Extensions
```

ミドルウェアパイプラインでUseSpaをします。

```cs
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) {

    app.UseSpaStaticFiles();
    // 略
    // ---
    app.UseEndpoints(endpoints => {
        endpoints.MapControllers();
    });
    
    app.UseSpa(options => {
#if DEBUG
        options.UseProxyToSpaDevelopmentServer(Configuration["services:frontend:http:0"]!);
    });
#endif
}
```

UseEndpointsよりも下に書くことで、コントローラでハンドリングされなかったリクエストはすべてこのSPAミドルウェアに流れていきます。

ConfigurationにAspireが指定したフロントエンドのHTTPエンドポイントが入っているので、その値を使ってUseProxyToSpaDevelopmentServerを呼び出すことで、viteの開発サーバにリクエストをプロキシさせることができます。

#if DEBUGで囲っているのは、本番環境時はwwwroot配下にフロントエンドのビルド成果物が入っているはずなのでデバッグ時のみだけプロキシするようにしています。


これで、同一のホスト上でバックエンドもフロントエンドも動作するようになります。

---

まだ導入したばかりですが、フロントエンドとバックエンドを同時開発する場面では非常に便利だなと感じています。

複数プロジェクトで似たような構成のものがあるので、横展開していこうと思っています。



# Oh my teethについて
[Oh my teeth](https://www.oh-my-teeth.com/?tags=qiita)では未来の歯科体験を創るために日々活動しています。

Techチームではより良いユーザー体験を提供するべく、Webフロントエンドからバックエンド、スマホアプリに機械学習モデルなど、さまざまなプロダクトを開発しています。

一緒に未来の歯科体験を創りませんか？興味がある方は是非こちらを確認してください。

https://oh.careers/

カジュアル面談も可能なので気軽に応募してみてください！

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3192685/0138b492-e02b-852e-61fa-fc065b53d285.png" width="450" />