2025-01-28

TypeSpec + OpenAPI を使った公開APIのスキーマ駆動開発

TypeSpec + OpenAPI を使った公開APIのスキーマ駆動開発

執筆: API総研編集部

公開API開発において、仕様と実装の一貫性を保ちながら効率的に進められる「スキーマ駆動開発」は、APIの利用者・開発者の双方にとって開発スピードや品質、開発者体験を高める有効なアプローチです。

スキーマ駆動開発を支える主な技術として、API仕様を記述するための標準規格であるOpenAPIが挙げられます。すでに多くの開発者がスキーマ駆動開発に利用しており、開発者ドキュメントやクライアント/サーバーコードのジェネレーターなど、周辺ツールも広く普及しています。

一方で、OpenAPIを使ったスキーマ駆動開発では、仕様の記述言語であるYAMLやJSONの言語特性や機能不足から、仕様のメンテナンス性や開発者体験の面で、まだ改善の余地があると言えます。

そこで本記事では、OpenAPIを使った公開APIのスキーマ駆動開発をさらに効率化し、開発者体験を向上させる選択肢として、TypeSpecを利用したスキーマ駆動開発の概要を紹介します。

TypeSpecとは

TypeSpecはスキーマ駆動開発を行うためのドメイン特化言語(DSL)であり、Microsoftが開発を進めています。TypeScriptに似た構文を使用してAPI仕様を記述できる点が特徴です。

typespec.io

主な特徴は以下のとおりです。

  • TypeScript風の構文

開発者に馴染みやすい文法であり、OpenAPIで用いるJSONやYAMLと比較して、API仕様をより直感的に記述できます。

  • 自動生成機能

TypeSpecで記述した仕様から、OpenAPIやProtocol Buffersなど、さまざまな形式のAPI仕様書を自動生成できます。

  • スキーマの可読性と保守性を向上させるさまざまな機能

import 構文により大規模なAPI仕様を複数ファイルに分割したり、全エンドポイントで共通となるリクエストパラメータを定義したり、新規作成系と更新系のエンドポイントで必須となるリクエストパラメータを変更したり、スキーマの可読性と保守性を向上するさまざまな言語機能が利用できます。

  • ツールサポート

Visual Studio Code用の専用拡張機能をインストールすることで、自動補完やエラーチェックなど、開発を支援する機能が利用できます。

  • 公開APIのサポート

認証方式や破壊的変更を考慮したバージョニングなど、公開APIの仕様を記述するうえで有用な機能を備えています。

TypeSpec を使った公開API仕様の作成

前提

  • Node.js v23.6.1
  • TypeSpec v0.64.0

TypeSpecのインストール

以下のコマンドを実行して、TypeSpec をインストールします。

npm install -g @typespec/compiler

プロジェクトのセットアップ

まず、プロジェクトディレクトリを作成します。

mkdir typespec-sample
cd typespec-sample

続いて、以下のコマンドを実行して TypeSpec のプロジェクトをセットアップします。

tsp init

セットアップ時には以下のような項目を尋ねられます。参考例として次のように選択してください。

  • Please select a template: Generic REST API を選択
  • Project name: typespec-sample
  • Update the libraries: そのままEnterを入力

セットアップが完了したら、以下のコマンドを入力し関連ライブラリをインストールします。

tsp install

これで main.tsp というファイルが生成されます。ここにAPI仕様を記述していきますが、初期状態ではまだ具体的な定義は含まれていません。

import "@typespec/http";
using TypeSpec.Http;

以下のコマンドを実行すると、TypeSpecファイルがコンパイルされ、tsp-output/@typespec/openapi3 ディレクトリに空のOpenAPI仕様 openapi.yaml が生成されます。また、--watch オプションを付けているため、main.tsp を更新するたびに自動的にOpenAPI仕様も更新されます。

tsp compile . --watch
openapi: 3.0.0
info:
  title: (title)
  version: 0.0.0
tags: []
paths: {}
components: {}

Visual Studio Code 拡張機能をインストール

エディタがVisual Studio Code(またはCursorなどの派生エディタ)の場合は、以下の拡張機能のインストールをおすすめします。

https://marketplace.visualstudio.com/items?itemName=typespec.typespec-vscode

API仕様を記述するTypeSpecファイル(.tsp)に関して、以下の機能が有効になります。

  • シンタックスハイライト
  • 自動フォーマット
  • コード補完
  • エラーチェック
  • コードジャンプ

API仕様の作成

本記事では、サンプルとしてユーザー情報を管理する公開APIの仕様を定義します。

なお、本記事では紹介しませんが、OpenAPI(version 3)の仕様からTypeSpecへの変換機能がTypeSpecから提供されています。既存のOpenAPI仕様からTypeSpecへの移行を行いたい場合は、以下のドキュメントを参照してください。

https://typespec.io/docs/emitters/openapi3/cli/

サービスの定義

まずは main.tsp にサービスの定義を追記します。

@service({
    title: "User Service",
})
@server("http://localhost:3000", "user service api endpoint")
// @versioned(Versions)
namespace UserService;

すると、自動生成されたOpenAPI 仕様は以下のように更新されます。

openapi: 3.0.0
info:
  title: User Service
  version: 0.0.0
tags: []
paths: {}
components: {}
servers:
  - url: http://localhost:3000
    description: user service api endpoint
    variables: {}

モデルの定義

続いて、main.tsp にユーザーモデルの定義を追記します。

model User {
    id: int32;
    name: string;
    age: int32;
    gender: genderType;
}

enum genderType {
    male: "male",
    female: "female",
}

すると、OpenAPI仕様には以下のように反映されます。

components:
  schemas:
    User:
      type: object
      required:
        - id
        - name
        - age
        - gender
      properties:
        id:
          type: integer
          format: int32
        name:
          type: string
        age:
          type: integer
          format: int32
        gender:
          $ref: '#/components/schemas/genderType'
    genderType:
      type: string
      enum:
        - male
        - female

エンドポイントの定義

次に、ユーザーモデルに対してAPIを介して提供する操作(=エンドポイント)を定義します。main.tsp に以下を追記して、ユーザー詳細、ユーザー新規作成の2つの操作を追加します。

@route("/users")
namespace Users {
    @get
    op getUser(@path id: int32): {
        @statusCode statusCode: 200;
        @body user: User;
    };

    @post
    op createUser(@body user: User): {
        @statusCode statusCode: 201;
        @body user: User;
    };
}

すると、OpenAPI仕様に以下の内容が追記されます。

paths:
  /users:
    post:
      operationId: Users_createUser
      parameters: []
      responses:
        '201':
          description: The request has succeeded and a new resource has been created as a result.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/User'
  /users/{id}:
    get:
      operationId: Users_getUser
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
            format: int32
      responses:
        '200':
          description: The request has succeeded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'

リクエストパラメータの微調整

このままでは、 POST /users でユーザーを新規作成する際にも id が必須になってしまいます。実際には、ユーザーIDは作成後に割り振られるため、作成リクエストを送信する際には id を指定できません。

ここで、TypeSpecのVisibility機能を利用すると、操作の種類(Read / Create / Updateなど)ごとに必須となるパラメータを動的に変更できます。

たとえば、main.tsp のモデル定義に @visibility Decoratorを付与すると、id パラメータが読み取りまたは更新操作のときだけ必須になるよう指定できます。

model User {
    @visibility(Lifecycle.Read, Lifecycle.Update) // ← 追記
    id: int32;

    name: string;
    age: int32;
    gender: genderType;
}

これにより、OpenAPI仕様は以下のように変化します。

paths:
  /users:
    post:
      operationId: Users_createUser
      # 中略
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UserCreate' # ← 新規作成エンドポイントだけ専用のスキーマを参照する形に変化
  /users/{id}:
    get:
      operationId: Users_getUser
      # 中略
      responses:
        '200':
          description: The request has succeeded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
components:
  schemas:
    User:
      type: object
      required:
        - id
        - name
        - age
        - gender
      # 中略
    UserCreate: # id が必須ではない新たなスキーマが新しく追加
      type: object
      required:
        - name
        - age
        - gender

公開API開発で便利に使える機能

バージョニング

一度公開したAPIに対して後方互換性のない変更を加える場合、APIバージョンを明示して管理するのが一般的です。たとえば以下のように、パスにバージョンを含める方法があります。

  • /v1/users
  • /v2/users

しかし、バージョン数や変更差分が増えると仕様の管理が煩雑になってきます。TypeSpecのVersioning機能を使うことで、一つの main.tsp 内で複数のバージョンを効率的に管理できます。

以下のように、versioning モジュールをインポートし、APIのバージョンを定義します。ここでは 1.02.0 の2つのバージョンを定義しています。

import "@typespec/http";
import "@typespec/versioning"; // 追記

using TypeSpec.Http;
using TypeSpec.Versioning; // 追記

@service({
    title: "User Service",
})
@server("http://localhost:3000", "user service api endpoint")
@versioned(Versions) // 追記
namespace UserService;

// 追記
enum Versions {
    v1: "1.0",
    v2: "2.0",
}

この設定をベースに、ユーザーモデルを以下のように変更します。ここでは破壊的変更として、プロパティ名の変更・追加・削除を行う例を示します。

model User {
    @visibility(Lifecycle.Read, Lifecycle.Update)
    id: int32;

    @renamedFrom(Versions.v2, "name") // v2 から "name" というフィールドを "firstName"に変更
    firstName: string;

    @added(Versions.v2) // v2 から追加
    lastName: string;

    age: int32;

    @removed(Versions.v2) // v2 から削除
    gender: genderType;
}

これにより tsp-output/@typespec/openapi3 ディレクトリ以下に、各バージョン別のOpenAPI仕様が出力されます。

  • openapi.1.0.yaml
  • openapi.2.0.yaml

openapi.1.0.yaml では、v2 で行った破壊的変更が反映されていません。

openapi: 3.0.0
info:
  title: User Service
  version: '1.0'
components:
  schemas:
    User:
      type: object
      required:
        - id
        - name
        - age
        - gender
      properties:
        id:
          type: integer
          format: int32
        name:
          type: string
        age:
          type: integer
          format: int32
        gender:
          $ref: '#/components/schemas/genderType'

一方の openapi.2.0.yaml では、バージョンが 2.0 となり、 firstName / lastName の追加や gender の削除など v2 の変更が反映されます。

openapi: 3.0.0
info:
  title: User Service
  version: '2.0'
# 中略
components:
  schemas:
    User:
      type: object
      required:
        - id
        - firstName
        - lastName
        - age
      properties:
        id:
          type: integer
          format: int32
        firstName:
          type: string
        lastName:
          type: string
        age:
          type: integer
          format: int32

このように、一つのTypeSpecファイルから複数バージョンのOpenAPI仕様を生成し、破壊的変更を見据えた管理が容易になります。バージョン管理に関するDecoratorは他にも用意されており、詳細はドキュメントを参照してください。

https://typespec.io/docs/libraries/versioning/reference/decorators/#@TypeSpec.Versioning.renamedFrom

認証方式の指定

TypeSpecが提供する @useAuth Decoratorを利用して、サービスやエンドポイント単位で認証方式を指定できます。たとえばUser Service全体を「APIキー認証」にしたい場合、以下のように @useAuth を追加し、X-API-KEY ヘッダー経由でAPIキーを送付するよう指定できます。

@service({
    title: "User Service",
})
@server("http://localhost:3000", "user service api endpoint")
@versioned(Versions)
@useAuth(ApiKeyAuth<ApiKeyLocation.header, "X-API-KEY">) // 追記
namespace UserService;

これにより、OpenAPI 仕様に以下の認証に関する内容が追記されます。

# 前略
security:
  - ApiKeyAuth: []
components:
	# 中略
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-KEY

@useAuth Decoratorでは、APIキー認証以外に「Bearer認証」「OAuth 2.0」など多様な方式を指定できます。詳細は以下のドキュメントを参照してください。

https://typespec.io/docs/libraries/http/authentication/

おわりに

本記事では、OpenAPI を利用したスキーマ駆動開発をより効率的に行う手段として、TypeSpec を紹介しました。 TypeSpec から生成された OpenAPI 仕様は、既存の開発ツールを用いて API ドキュメント、クライアントコード、サーバーコード、バリデーションロジックなどの自動生成にそのまま活用できます。これまでのツールチェーンや開発プロセスを大きく変えずに、開発者体験や効率を改善できるのも利点です。

また、OpenAPI 仕様から TypeSpec への変換機能も提供されているため、既存プロジェクトへの導入ハードルも比較的低いでしょう。スキーマ駆動開発を実現する上での選択肢の一つとして、TypeSpec を検討してみてはいかがでしょうか。

本メディアはAPI特化の開発会社であるECU株式会社が運営しています。記事についてのご質問やAPIに関するご相談・お問い合わせはお気軽にお問い合わせフォームまで。

SNSでシェア

はてなブックマーク