OpenAPIの定義を使ってTypeScript(axios)のクライアントコードを生成してsvelteで利用する

実験用APIサーバを作る

FastAPIで作るのが一番簡単だと思うので、FastAPIを使う。 この後の環境構築まで一通り終わってからちゃんとコードを書くようにしたいので、まず公式のチュートリアルにあるコードをコピーしつつ、あとあとの事情により一箇所だけ書き換えてapiserver/main.pyと言う名前で保存

from fastapi import FastAPI

app = FastAPI()


@app.get("/api/v0/")
async def root():
    return {"message": "Hello World"}

svelteでアプリを作る

公式サイトと同じ感じで。作成中に聞かれる質問に対してTypeScriptを使う選択をしておく。

npm create svelte@latest app
cd app
npm install

静的コンテンツとしてnginxで配信したいので、app/svelte.config.jsを以下のように書き換える。

import adapter from "@sveltejs/adapter-static";
import preprocess from "svelte-preprocess";

/** @type {import('@sveltejs/kit').Config} */
const config = {
  // Consult https://github.com/sveltejs/svelte-preprocess
  // for more information about preprocessors
  preprocess: preprocess(),

  kit: {
    trailingSlash: "never",
    adapter: adapter({
      pages: "build",
      assets: "build",
      fallback: "index.html",
      precompress: false,
    }),
  },
};

export default config;

アダプタとしてadapter-staticを使うことになったので、インストール。app配下で以下のコマンドを実行。

npm install -D @sveltejs/adapter-static

そしてビルド。同じくapp配下で、以下のコマンドを実行する。

npm run build

これでapp/build配下に静的サイトのファイルが生成されたはず。

docker-composeで起動する

APIサーバのソースと静的サイトが取り合えずできたので、docker-composeを使ってAPIサーバを起動しつつ、nginxも起動して静的コンテンツの配信とAPIサーバへのリクエストの中継をやらせる。

nginxの設定ファイルとして以下のようなものを準備してconf.d/default.confと言う名前で保存しておく。

server {
    listen       80;
    listen  [::]:80;
    server_name  localhost;

    access_log  /dev/stdout  main;
    error_log   /dev/stderr  warn;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
        try_files $uri /index.html;
    }
    
    location /api/v0/ {
        proxy_pass http://apiserver/api/v0/;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

}

apiserverを起動するためのDockerfileをapiserver/Dockerfileとして保存

FROM tiangolo/uvicorn-gunicorn-fastapi:python3.9
COPY ./app /app

そしてdocker-compose.yamlファイルを作成

services:

  nginx:
    container_name: nnnnnnnginx
    image: nginx:latest
    volumes:
      - ./conf.d:/etc/nginx/conf.d
      - ./app/build:/usr/share/nginx/html
    ports:
      - "80:80"
    depends_on:
      - "apiserver"

  apiserver:
    container_name: aaaaaapiserver
    build:
      context: ./apiserver
      dockerfile: Dockerfile
    ports:
      - "3000:80"

そしてdocker-compose upすればhttp://localhostへのアクセスで先ほど作った静的サイトが表示され、http://localhost/api/v0/にアクセスすればAPIサーバが反応してくれることでしょう。

アプリからAPIサーバを呼び出す

APIサーバからの応答を画面に表示するように変更する。まずapp/src/routes/+page.svelteを書き換えて以下のようにする。

<script lang="ts">
    const getMessage = async () => {
        let res = await fetch("/api/v0/");
        let data = await res.json();
        return data.message;
    }
</script>
{#await getMessage()}
  now loading...
{:then message}
  {message}
{/await}

再度ビルドしてからブラウザでhttp://localhostにアクセスすると、先ほどAPIサーバから受け取ったのと同じメッセージがブラウザ上に表示されるはずだ。

OpenAPIの定義を使ってクライアント側のTypeScriptのコードを生成する

http://localhost:3000/docs にアクセスしてみると、FastAPIがAPI仕様書(swagger-ui)を送り返してくれる。このページの左上あたりに/openapi.jsonと書かれたリンクがあるので、これをファイルに落とす。

中身を見るとこんな感じになっていることであろう。(これを手で書きたくないからFastAPIを使った)

{"openapi":"3.0.2","info":{"title":"FastAPI","version":"0.1.0"},"paths":{"/api/v0/":{"get":{"summary":"Root","operationId":"root_api_v0__get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}}}}

この保存したファイルに対してコマンドを実行してソースコードを生成させる。これがベストプラクティスなのかは不明だが、とりあえず出力はできたのでここではよしとする。

docker run --rm -v ${PWD}:/local -v ${PWD}/app/src/oapi:/local/lib openapitools/openapi-generator-cli generate -i /local/openapi.json -g typescript-axios -o /local/lib

これでapp/src/oapi 配下にtypescript用のaxiosを用いたAPIクライアントが生成されたはず。

生成されたコードを使ってAPIにアクセスしてみる

app/src/routes/+page.svelteを書き換えて、生成したコードに含まれる関数を使ってみる。

<script lang="ts">
  import {DefaultApi } from '../oapi/api';
  import { Configuration } from '../oapi/configuration';
  import axios from 'axios';

  const cl = axios.create();
  const api = new DefaultApi(new Configuration(), '', cl);

  const getMessage = async () => {
    const resp = await api.rootApiV0Get()
    return resp.data.message;
  }
</script>
{#await getMessage()}
  now loading...
{:then message}
  {message}
{/await}

これでビルドしてリロードすると、先ほどと同じ内容の文字列が画面に表示される。自力でfetchするのではなくて、OpenAPIの定義から自動生成された関数を使って同じ情報をAPIサーバから取得できたことがわかる。