OLTA TECH BLOG

テクノロジーと好奇心で事業を成長させる

TECH BLOG

Django REST frameworkとOpenAPI + Nuxt TypeScriptで実現するAPI型自動生成のために取り組んだこと

概要

こんにちは、INVOYでフロントエンドエンジニアを担当している小林です. 今回は、INVOYのフロントエンド開発において、OpenAPIを利用してAPIの型定義を自動生成した取り組みをご紹介したいと思います.この自動化により効率的なAPIの型定義の生成が実現できました.

INVOYのフロントエンドはNuxt.jsとVue.js、そしてバックエンドはPythonDjangoで構築されています.

対象読者

  • JavaScriptからTypeScriptに移行したい人
  • Django REST frameworkを利用している人
  • Django REST frameworkでOpenAPIのファイルを生成したい人

INVOYのTypeScriptの現状・課題

これまでINVOYのフロントエンドではTypeScriptを導入していたものの、コードの8割以上はJavaScriptで記述されていました.TypeScriptへの移行は重要な課題でしたが、現状以下の問題によりその実現は困難な状況でした.

  • API呼び出しの非共通化: 同一URLへのAPIリクエストが await apiFetch("url") のように記述されており、共通のAPIフェッチ関数が作成されていませんでした.ここを設計したい!
  • APIの型定義が存在しない: APIリクエストに対してどのような値が返ってくるかの仕様書がなかったため、TypeScriptの型定義の実装に時間が取られました.ここを設計したい!

API呼び出しの非共通化の問題についての具体的なコードは以下の通りです.

// a.vue
const aFunc = async () => {
  dataA.value = await apiFetch(
    `/api/data`,
    { method: 'GET' }
  );
};

// b.vue
const bFunc = async () => {
  dataB.value = await apiFetch(
    `/api/data`,
    { method: 'GET' }
  );
};

上記の例では、a.vueb.vueという異なるコンポーネント内で、同じ/api/dataというURLに対してGETリクエストを行うAPI呼び出しがそれぞれ記述されています.本来であれば、このような共通のAPIエンドポイントへのアクセスは、再利用可能な関数として共通化されるべきです.各コンポーネント内部で個別にAPI呼び出しを記述しているため、それぞれのAPIに対して型を付与する必要があり開発効率の低下を招いてしまいます.

このように、まずTypeScript化を進めるためには「APIの共通化を進める必要」があるとともに並行して「バックエンドのAPIレスポンスの型定義を自動作成する」というより根本的な課題にも向き合う必要がありました.

どうやるか

APIの共通化を進める

この問題に対して、現状のAPI呼び出しの基本的な構造は維持しつつ最小限の変更で共通化を実現します.これによりチーム全体にスムーズにこの考え方を浸透させることを目指します.

具体的には以下のコードです.

const useDataApi = () => {
  const apiFetch = useApiFetch()
  
  getDataDetail: (id:string)=> {
    return apiFetch<GenType>(`/api/data/${id}`)
  }
  
  getDataList: (requestParams: object) => {
    return apiFetch<GenType>(`/api/data/`, {
      query: requestParams
    })
  }
  
  postData: (payload: GenType) => {
    return apiFetch<GenType>(`/api/data/`, {
      method: "POST",
      body: {
        ...payload
      }
    })
  }
  
  return { getDataDetail, getDataList, postData }
 }
}

これをVue.jsのsetupで呼び出し利用します.もちろん、この useDataApiはTypeScriptで記述されているためAPIのレスポンスやリクエストの型を定義でき、型安全なAPI連携を実現できます.

バックエンドのAPIレスポンスの型定義を自動作成する

フロントエンドの型自動生成のため、バックエンドではDjangodrf-spectacularを導入しました.Serializer定義などからOpenAPIスキーマを自動生成できますが@propertySerializerMethodFieldのようなケースでは、デフォルトでstring型が生成されます.

通常、この型の不一致は@extend_schema_fieldなどのアノテーションで修正可能です.しかし、INVOYのプロダクション環境ではセキュリティ上の理由からdrf-spectacularをインストールしておらずこれらのアノテーションを利用できませんでした.

そのため、標準ライブラリの機能に頼るのではなく、「drf-spectacularを拡張しPythonの型ヒントを利用」して誤った型を修正する独自の仕組みを実装しました.

  • フロー:Serializer定義 + Pythonの型ヒントを付与拡張されたdrf-spectacularを実行修正されたOpenAPIスキーマの出力
  • コード:具体的なコードのPython型ヒントのイメージと生成されるyamlは以下の通りです.
class DataSerialiser:
  data = SerializerMethodField()
  
  # @extend_schema_field(DataSerializer) (本来こんな感じで書く必要がある)
  def get_data(self) -> DataSerializer #返り値のtypeを付与することでyamlに自動で生成される
    # 何かしらの処理
    return serializer.data


#生成後は以下の形になる
DataSerialiserResponse:
  type: object
  properties:
    data:
      type: object
      properties:
        data1:
          type: integer
          required: true
          nullable: true
        data2:
          type: integer
          required: true
        data3:
          type: integer
      required:
      - data1
      - data2
      - data3
  required:
  - data

この方法を応用することでAPIのリクエストやレスポンスに対しても以下のように設定することができます

# viewのactionデコレータ
@action
def create_data(self, request) -> DataSerializer:

# 通常のapiの関数 (SerializerTypedRequestに対してSerializerを設定することでrequestbodyのschemaを作れる)
def post(self, request:SerializerTypedRequest[DataSerializer]) -> DataSerializer:

OpenAPIからTypeScriptのtype生成へ

OpenAPIのYAMLファイルからTypeScriptの型を生成するフレームワークは数多く存在します.

これらのうち使いやすさ型の参照方法といった点を比較検討したのち採用したのは swagger-typescript-api でした.openapi-typescriptも候補として検討しましたが、型参照の方法が paths["/api/data"]["get"]["responses"][200]["content"]["application/json"]["schema"]のように直感的ではなく、導入は見送りました.

swagger-typescript-apiswagger-typescript-api --no-client --union-enums -o ./types/generated -p ./schema.yml -n generate.type.tsというシンプルなコマンドで、我々が欲しかった型を自動生成してくれたため非常に有用でした. 結果的に以下のようなtypeを自動生成することができました.これは、/api/dataエンドポイントのGETリクエストに対するレスポンスの型定義となります.

//generate.type.ts
export interface GenDataResponse {
  data1: string | null;
  data2: string;
  data3: string;
}

これを、API呼び出し関数に適用することで煩雑な型定義作業から解放され、より効率的に開発を進めることができるようになりました☺️

備考 swagger-typescript-api はOpenAPIスキーマに基づいてAPIクライアント(fetch関数など)を自動生成する機能も備えています.しかし、この機能は現段階では利用しないことにしました.理由は主に2つあります.

  • 独自のOpenAPIスキーマ生成: INVOYでは、drf-spectacularの標準機能だけでなく独自拡張も加えてOpenAPIスキーマを生成しています.そのため、生成される型が必ずしも完全に正確であるとは限りません.
  • 型修正の可能性: 上記の理由から、自動生成された型に修正が必要になる可能性を考慮しAPIクライアントまで自動生成してしまうと、その後のメンテナンスが煩雑になる恐れがありました.

終わりに

今回はDjango REST frameworkとOpenAPI + Nuxt TypeScriptで実現するAPI型自動生成のために取り組んだことについて記載しました. まだまだ導入したてなので、apiの共通化は少ししかできていませんが今後加速させていきます.

OLTAではユーザーに価値を提供し、事業を成長させるサービスを一緒に作る仲間を募集しています. もし、この投稿にご興味を持っていただけたら、是非カジュアルにお話しさせてください.

corp.olta.co.jp

参考文献

以下はtypeの自動生成を考える際に参考にさせていただきました🙇