型情報の効果的な活用:API を介してバックエンドとフロントエンドを繋ぐ

こんにちは!Insight Edgeの小林まさみつです。
Insight Edgeでは、単一のプロジェクトでバックエンドとフロントエンド両方の開発を担当することがあります。 開発時にはバックエンドとフロントエンドをうまく連携することが求められます。 その際、それぞれで型情報を定義すると多重管理することになり、管理の手間がかかることに加えて整合性が保ちづらくなります。
本記事では、型情報を含むAPIをスムーズに連携することでこれらの問題を解決し、開発プロセス全体の効率化を実現する方法を紹介します。

目次

1. 概要

型付けの重要性

型情報を明示することは、ソフトウェアをより堅牢かつ安全なものとするために重要です。 コードの可読性向上、開発者間のコミュニケーションの明確化、誤解の減少などのメリットがあります。 特に、バックエンドとフロントエンドが同じ型情報を共有することは、APIを介したシステム連携において重要な役割を果たします。

バックエンド・フロントエンド間の型情報連携の重要性

型情報の連携は、複数のシステム(今回はバックエンドとフロントエンド)が互いに連動するプロジェクトにおける整合性を保つ上で欠かせません。 型情報を共有することで、多重管理を避け修正もれや開発スピードの向上が見込めます。

2. 使用する主要な技術

本記事では下記の技術スタックを利用してスムーズな型情報連携を実現します。 openapi-zod-clientを除き、主要な技術については理解している前提で話を進めます。

バックエンド

Pydanticに関しては本techblogの以下の記事にも登場しているのでご一読ください。

フロントエンド

3. 本記事で扱うデータモデル

本記事では、サンプルとして「User」というデータモデルを利用します。 Userモデルは、名前(name)と年齢(age)という2つの属性を持ちます。 ここでの型付けルールは以下の通りです。

  • 名前:1文字以上かつ、10文字以下の文字列であること。
  • 年齢:0歳から130歳までの整数であること。

また、このデータを用いて以下の流れで開発していきます。

③〜④の部分で、バックエンドで定義した型情報を含むAPIをフロントエンドに連携させます。

4. バックエンドの型情報と FastAPI の役割

ディレクトリ構造について

本記事では、以下のディレクトリ構成となるように進めていきます。 プロジェクトに導入する際には、プロジェクトごとに管理しやすいディレクトリ構成としてください。

/
├── backend/
│   └── app.py
├── frontend/
│   ├── index.ts
│   └── services/
│       └── openapiClient.ts # 自動で生成する
├── build_openapi.py
├── openapi.json # 自動で生成する
├── package.json
├── pnpm-lock.yaml
├── poetry.lock
└── pyproject.toml

①Pydanticモデルの作成

APIでやり取りをする際にどのような情報を扱うかを明確とするために、まずはPythonで型情報を定義します。 その際、バリデーションを行い型安全を保証するためにPydanticモデルを作成します。
初めにPydanticをインストールします。

poetry add pydantic

次に、Userクラスを作成しモデル定義をします。

# ./backend/app.py
from pydantic import BaseModel, Field

class User(BaseModel):
    name: str = Field(..., min_length=1, max_length=10)
    age: int = Field(..., ge=0, le=130)

以上で、要件を満たすPydanticモデルの定義ができました。 このようにすることで条件を満たさない入力がUserに対して行われた際、例外を返すようになります。

②APIルートの作成

次に、作成したPydanticモデルを取得するためのAPIを作成します。 まずはFastAPIとそれを実行するためのuvicornをインストールします。

poetry add fastapi uvicorn

次に、FastAPIのルートを定義します。 今回は簡単にするため、APIは以下の仕様とします。

  • ルートへのgetリクエストを受け取り、固定のUserを返却する。
# ./backend/app.py (追記)
from fastapi import FastAPI

app = FastAPI()

@app.get("/", response_model=User)
def get_user() -> User:
    return User(name="masam", age=20)

以下のコマンドを実行し、アプリを起動します。 その後、ブラウザからアクセスすることでAPIを呼び出せることを確認します。

poetry run uvicorn backend.app:app --host 0.0.0.0

http://localhost:8000/ にアクセスすると以下の内容が表示されます。

{"name":"masam","age":20}

③OpenAPIドキュメントの生成

作成したPydanticモデルとFastAPIルートを基にOpenAPIドキュメントを生成します。 OpenAPIドキュメントはFastAPIの機能を用いることで生成できます。

ルートディレクトリに build_openapi.py を作成し、以下を記述します。

./build_openapi.py
import json
from backend.app import app

with open('openapi.json', 'w') as f:
    json.dump(app.openapi(), f, ensure_ascii=False)

その後 build_openapi.py を実行します。

python build_openapi.py

ルートディレクトリに openapi.json が生成されていることを確認します。 作成したAPIのコードからOpenAPIドキュメントを生成できました。 以上でバックエンド側の作業は完了です。

5. フロントエンド開発の効率化

④Zodスキーマの生成

生成したOpenAPIドキュメントを基にZodスキーマとAPIクライアントを生成します。 まずは生成するためのライブラリと、ターミナル上でTypeScriptを実行するためのライブラリをインストールします。

pnpm add openapi-zod-client zod @zodios/core ts-node

次に、簡単に生成できるようにするため、 package.json のscriptsに以下を追加します。

"generate-openapi-zod-client": "openapi-zod-client ./openapi.json -o frontend/services/openapiClient.ts -a"

上記コマンドを実行する前に、生成するクライアントコードを配置するディレクトリを作成します。

mkdir -p frontend/services

これで準備が整いました。 設定したスクリプトを実行し、必要なコードが生成されることを確認します。

pnpm generate-openapi-zod-client

以下のようなコードが生成されます。

frontend/services/openapiClient.ts

import { makeApi, Zodios, type ZodiosOptions } from "@zodios/core";
import { z } from "zod";

const User = z
  .object({
    name: z.string().min(1).max(10),
    age: z.number().int().gte(0).lte(130),
  })
  .passthrough();

export const schemas = {
  User,
};

const endpoints = makeApi([
  {
    method: "get",
    path: "/",
    alias: "get_user__get",
    requestFormat: "json",
    response: User,
  },
]);

export const api = new Zodios(endpoints);

export function createApiClient(baseUrl: string, options?: ZodiosOptions) {
  return new Zodios(baseUrl, endpoints, options);
}

⑤Zodを用いた開発

次に、生成されたAPIを実行するコードを作成します。 今回はAPIから受け取ったデータをコンソールに出力するだけにします。

// frontend/index.ts
import { z } from "zod";
import { createApiClient, schemas } from "./services/openapiClient";

const BASE_URL = "http://localhost:8000";

type UserType = z.infer<typeof schemas.User>;

const getUser = async () => await createApiClient(BASE_URL).get_user__get();

getUser().then((user: UserType) => {
  console.log(`name: ${user.name}, age: ${user.age}`);
});

以下のコマンドで実行し、データを受け取れることを確認します。 ※取得できない場合はFastAPIを動かすuvicornが起動していない可能性があるので再度ご確認ください。

npx ts-node frontend/index.ts

6. バックエンドとフロントエンド間の型同期

これまでの内容により、バックエンドで定義した型情報をフロントエンドに取り込むことができました。 システム開発においては1度取り込んで終わりというわけにはいきません。 システム改修に伴い型情報や、APIでやり取りするモデルを変更することが多々あります。 特に、バックエンドとフロントエンドの両方を並行して開発している際には高頻度でモデルが変わります。 その際にモデル変更に対して素早く追随するために、簡単に同期をとるためのコマンドを開発サイクルに組み込むと良いです。 今回作成したコードを利用すると、以下の2コマンドを実行することで実現できます。

python build_openapi.py
pnpm generate-openapi-zod-client

7. 注意点

現時点ではPydanticで定義したカスタムバリデーションをZodへ同期できていません。 そのため、厳密なバリデーションに関しては追加で実装するか、諦めてAPI側で担保するかの方針を決める必要があります。 この問題に対する解決策をご存じの方がいらっしゃれば、ぜひフィードバックをお寄せください。

8. まとめと今後の展望

バックエンドで定義した型情報を活用してフロントエンドコードを簡単に実装する方法を解説しました。 この連携がスムーズに行われれば、バグのリスクを減らしつつ開発速度を劇的に向上させることができます。 今後もAPIを介した型情報の活用して開発速度を上げていこうと思います。