FastAPIにSPAアプリケーションを統合する

# はじめに

この記事では、FastAPIを使用してSPA（シングルページアプリケーション）を統合する方法について検証します。
FastAPIは主にバックエンドAPIサーバーとして使用されますが、SPAと統合することで単一のアプリケーションとして動作させることができます。

# FastAPIにSPAを統合する

FastAPIには静的ファイルを配信する機能があります。
[FastAPIの静的ファイル配信機能](https://fastapi.tiangolo.com/ja/tutorial/static-files/)

例えば、以下のようなコードを`main.py`に追加することで、`static`ディレクトリにあるファイルを配信することができます。

```python
app = FastAPI(
    title="FastAPI, SPA",
    description="FastAPIでSPAを作成する",
    version="1.0.0",
)

app.mount("/", StaticFiles(directory="static"), name="static")
```

そのため、Nuxt.jsで作成したSPAのフロントエンドを`static`ディレクトリに配置することで、FastAPIに統合することができます。

## SPAのフロントエンドをNuxt.jsで作成する

Nuxt.jsを使用してSPAのフロントエンドを作成する手順を説明します。
まず、Nuxt.jsプロジェクトを初期化します。

```bash
npx nuxi init --package-manager yarn frontend
```

その後、`nuxt.config.ts` の `ssr: false` を指定し、SPAであることを明示します。

```ts
export default defineNuxtConfig({
  ...
  ssr: false,
})
```

その後、Nuxt.jsプロジェクトを起動し、
`localhost:3000`にアクセスして、Nuxt.jsプロジェクトが正常に起動していることを確認します。

```bash
cd frontend
yarn dev
```

### Vuetifyを使用するように設定する

Nuxt.jsでVuetifyを使用するように設定します。

```bash
yarn add vuetify @mdi/font 
```

**nuxt.config.ts**
```ts
import vuetify, { transformAssetUrls } from 'vite-plugin-vuetify'

export default defineNuxtConfig({
  ...
  build: {
    transpile: ['vuetify'],
  },
  modules: [
    (_options, nuxt) => {
      nuxt.hooks.hook('vite:extendConfig', (config) => {
        // @ts-expect-error
        config.plugins.push(vuetify({ autoImport: true }))
      })
    }
  ],
  vite: {
    vue: {
      template: {
        transformAssetUrls,
      },
    },
  },
})
```

**plugins/vuetify.ts**
```ts
import '@mdi/font/css/materialdesignicons.css'

import 'vuetify/styles'
import { createVuetify } from 'vuetify'

import * as components from 'vuetify/components'
import * as directives from 'vuetify/directives'

export default defineNuxtPlugin((app) => {
  const vuetify = createVuetify({
    ssr: false,
    components,
    directives,
  })
  app.vueApp.use(vuetify)
})
```

### フロントエンドのページ作成

Nuxt.jsでページを作成するには、`pages`ディレクトリにファイルを作成します。

```bash
mkdir -p pages/index.vue
```

**pages/index.vue**
```vue
<template>
  <div>
    <h1>Home Page</h1>
    <p>Welcome to the Home page!</p>
  </div>
</template>
```

**pages/about.vue**
```vue
<template>
  <div>
    <h1>About Page</h1>
    <p>Learn more about us on this page.</p>
  </div>
</template>
```

### フロントエンドのレイアウト作成

Nuxt.jsでレイアウトを作成するには、`layouts`ディレクトリにファイルを作成します。


```bash
mkdir -p layouts/default.vue
```

**layouts/default.vue**
```vue
<template>
  <v-app>
    <v-container :class="{ 'drawer-open': drawer }">
      <v-row>
        <v-col>
          <v-app-bar app>
            <v-btn icon @click="drawer = !drawer">
              <v-icon>mdi-menu</v-icon>
            </v-btn>
            <v-toolbar-title>My Website</v-toolbar-title>
            <v-spacer></v-spacer>
            <v-btn text to="/">Home</v-btn>
            <v-btn text to="/about">About</v-btn>
          </v-app-bar>
          <v-navigation-drawer app v-model="drawer">
            <v-list>
              <v-list-item link to="/">
                <v-list-item-title>Home</v-list-item-title>
              </v-list-item>
              <v-list-item link to="/about">
                <v-list-item-title>About</v-list-item-title>
              </v-list-item>
            </v-list>
          </v-navigation-drawer>
        </v-col>
      </v-row>
      <v-row>
        <v-col>
          <v-main>
            <slot />
          </v-main>
        </v-col>
      </v-row>
      <v-row>
        <v-col>
          <v-footer app fixed class="justify-center">
            <p>&copy; 2023 My Website</p>
          </v-footer>
        </v-col>
      </v-row>
    </v-container>
  </v-app>
</template>

<script>
export default {
  data() {
    return {
      drawer: false,
    };
  },
};
</script>

<style>
.drawer-open {
  margin-left: 256px;
}
</style>
```

以下のVuetifyコンポーネントが使用されています：

1. **v-app**: アプリケーションのルートコンポーネントで、他のすべてのVuetifyコンポーネントを包含します。
2. **v-app-bar**: アプリケーションの上部にナビゲーションバーを表示します。ここにはホームページとアバウトページへのリンクが含まれています。
3. **v-navigation-drawer**: サイドバーで、ナビゲーションリンクを含んでいます。このドロワーは動的に表示または非表示にすることができます。
4. **v-list** と **v-list-item**: ナビゲーションドロワー内で使用され、ナビゲーションリンクのリストを表示します。
5. **v-main**: ページの主要コンテンツを表示するためのコンテナです。スロットを使用して、ページ固有のコンテンツを挿入できます。
6. **v-footer**: フッター部分で、著作権情報などを表示します。

### app.vueの修正

**<NuxtLayout>** と **<NuxtPage>** を使用して、ページのレイアウトとコンテンツを管理します。これにより、Nuxt.jsのページコンポーネントを動的に表示することができます。

**app.vue**
```vue
<template>
  <NuxtLayout>
    <NuxtPage />
  </NuxtLayout>
</template>
```

### Nuxt.jsプロジェクトを起動する
上記の修正を行った内容が反映されているかを確認するために、Nuxt.jsプロジェクトを起動します。

```bash
yarn dev
```

Nuxt.jsプロジェクトが起動したら、ブラウザで`http://localhost:3000`にアクセスして、SPAのフロントエンドが正常に表示されることを確認します。

![index.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/479886/afae3b8d-664c-665e-1db9-be668d90446e.png)

![about.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/479886/13a9b231-e285-7028-363e-2ebe948a6c50.png)


## Nuxt.jsでビルドする

Nuxt.jsでビルドを行い、静的ファイルを生成します。

```bash
yarn generate
```

上記コマンドで、`output/public`ディレクトリに静的ファイルが生成されます。
(distディレクトリには、output/publicへのシンボリックリンクが作成されるだけのため、コピーする場合は注意が必要です。)

## FastAPIに静的ファイルを配信する

※FastAPIの前提は以下記事リンク先の内容です。
[Databricks Apps でカスタムアプリのデプロイ](https://qiita.com/kei1-dev/items/3e749e2519535f8c0d56)

FastAPIに静的ファイルを配信するには、`main.py`に以下のコードを追加しつつ、既存APIのパスを変更します。

```python
from fastapi import FastAPI, HTTPException
from fastapi.staticfiles import StaticFiles

app = FastAPI()

@app.get("/api")
def read_api_root():
    return {"Hello": "World"}

@app.get("/api/items/{item_id}")
def read_item(item_id: int, q: str = None):
    return {"item_id": item_id, "q": q}

app.mount("/", StaticFiles(directory="./static", html=True), name="static")

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)
```

`app.mount("/", StaticFiles(directory="./static", html=True), name="static")` 

この行は、FastAPIアプリケーションに静的ファイルを配信するための設定を追加します。ここで、"/"というルートパスに対して、"./static"ディレクトリ内の静的ファイルを配信するように設定しています。html=Trueオプションを指定することで、HTMLファイルを適切に扱うことができます。

また、backend側staticディレクトリを作成し、Nuxt.jsでビルドしたoutput/publicディレクトリ内の静的ファイルをコピーします。

上記対応後、以下コマンドで、FastAPIアプリケーションを起動します。

```bash
gunicorn main:app -k uvicorn.workers.UvicornWorker -w 1
```

ブラウザで`http://localhost:8000`にアクセスして、SPAのフロントエンドが正常に表示されることを確認します。

![index.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/479886/afae3b8d-664c-665e-1db9-be668d90446e.png)

![about.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/479886/13a9b231-e285-7028-363e-2ebe948a6c50.png)

また、`http://localhost:8000/api/`にcurlでアクセスして、APIが正常に動作していることを確認します。

```bash
curl http://localhost:8000/api/
# {"Hello": "World"}
curl http://localhost:8000/api/items/1?q=hoge
# {"item_id": 1, "q": "hoge}
```

これで、FastAPIで既存のバックエンドAPIエンドポイントとSPAのフロントエンドを統合することができました。

# まとめ

FastAPIとNuxt.jsを使用してSPAを統合する方法を検証しました。
FastAPIの静的ファイル配信機能を利用して、Nuxt.jsでビルドしたSPAのフロントエンドを配信することで実現できました。
この対応でフロントエンドとバックエンドが一つのアプリとして共存することが可能になりました。

# 参考

- [FastAPIの静的ファイル配信機能](https://fastapi.tiangolo.com/ja/tutorial/static-files/)
- [Nuxt.jsインストール](https://nuxt.com/docs/getting-started/installation)