This article was translated from Japanese by AI and may contain inaccuracies. For the most accurate content, please refer to the original Japanese version.
ダウンジャケットのイラスト

TypeScript のような構文で OpenAPI のスキーマを定義する TypeSpec

TypeSepc は TypeScript にインスパイアされた言語で、開発者が親しみやすい構文で OpenAPI のスキーマを定義できます。モデルを使ってデータの構造を定義し、`@route` デコレーターを使って REST API のエンドポイントを定義します。

TypeSpec は TypeScript にインスパイアされた言語で、開発者が親しみやすい構文で OpenAPI v3.0 のスキーマを定義できます。

スキーマの定義は以下のようになります。

main.tsp
import "@typespec/http";
 
using TypeSpec.Http;
 
model User {
  id: string;
  name: string;
  birthday?: utcDateTime;
  address: Address;
}
 
model Address {
  street: string;
  city: string;
  state: string;
  zip: string;
}
 
@route("/users")
interface Users {
  list(@query limit: int32, @query skip: int32): User[];
  create(@body user: User): User;
  get(@path id: string): User;
}

上記のコードをコンパイルすると、以下のような OpenAPI のスキーマが出力されます。

openapi.yaml
openapi: 3.0.0
info:
  title: (title)
  version: 0000-00-00
tags: []
paths:
  /users:
    get:
      operationId: Users_list
      parameters:
        - name: limit
          in: query
          required: true
          schema:
            type: integer
            format: int32
        - name: skip
          in: query
          required: true
          schema:
            type: integer
            format: int32
      responses:
        '200':
          description: The request has succeeded.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/User'
    post:
      operationId: Users_create
      parameters: []
      responses:
        '200':
          description: The request has succeeded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/User'
  /users/{id}:
    get:
      operationId: Users_get
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The request has succeeded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
components:
  schemas:
    Address:
      type: object
      required:
        - street
        - city
        - state
        - zip
      properties:
        street:
          type: string
        city:
          type: string
        state:
          type: string
        zip:
          type: string
    User:
      type: object
      required:
        - id
        - name
        - address
      properties:
        id:
          type: string
        name:
          type: string
        birthday:
          type: string
          format: date-time
        address:
          $ref: '#/components/schemas/Address'

また、TypeSpec は OpenAPI 形式だけでなく、JSON Schema や Protobuf、JSON RPC などの形式にも対応しています。

インストール

はじめに、TypeSpec コンパイラをインストールします。

npm install -g @typespec/compiler

以下のコマンドで tsp のプロジェクトを初期化します。

tsp init

テンプレートを選択するように聞かれるので、Generic Rest API を選択します。

ライブラリはデフォルト選択のまま @typespec/http@typespec/rest@typespec/openapi3 を選択します。

? Update the libraries?
Instructions:
    ↑/↓: Highlight option
    ←/→/[space]: Toggle selection
    a: Toggle all
    enter/return: Complete answer
   @typespec/http
   @typespec/rest
   @typespec/openapi3

セットアップが完了すると、以下のファイルが生成されます。

.
├── main.tsp
├── package.json
└── tsconfig.yaml

.tsp 拡張子は TypeSpec のソースコードを表します。もし VSCode を使っている場合には、以下の拡張をインストールすることをおすすめします。

以下のコマンドでコンパイルします。

tsp compile .

デフォルトの状態では tsp-ouput/@typespec/openapi3 に OpenAPI のスキーマが出力されます。

title=tsp-ouput/@typespec/openapi3/openapi.yaml
openapi: 3.0.0
info:
  title: (title)
  version: 0000-00-00
tags: []
paths: {}
components: {}

REST API のスキーマを定義してみる

それでは TypeSpec で REST API のスキーマを定義していきましょう。tsp compile . --watch コマンドを実行すると、ファイルの変更を検知して自動的にコンパイルしてくれます。

HTTP API とのバインディングには @typespec/http ライブラリを使います。このライブラリには HTTP のルーティングを定義するための @route デコレーターや、Body リクエストを表す body モデルなどが含まれています。

ライブラリをインポートするには import キーワードを使います。TypeScript の import と同じく、ファイルパスが指定された場合には対応するファイルまたはディレクトリから探索されます。パッケージ名が指定された場合にはまず TypeSpec は package.json を探索し、ライブラリのエントリーポイントを探します。

main.tsp
import "@typespec/http";

using キーワードを使うことで namespace を現在のスコープに公開できます。これにより、@route デコレーターなどパッケージで公開されているシンボルを参照できるようになります。

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

メタデータの定義

namespace に対して @typespec/http パッケージが提供する @service@server デコレーターを使うことで、OpenAPI のメタデータを定義できます。

main.tsp
import "@typespec/http";
 
using TypeSpec.Http;
 
@service({
  title: "User API",
  version: "1.0.0",
})
@server("https://example.com/api", "production")
namespace UserAPI;

コンパイルすると以下のように変換されます。

openapi.yaml
openapi: 3.0.0
info:
  title: User API
  version: 1.0.0
tags: []
paths: {}
components: {}
servers:
  - url: https://example.com/api
    description: production
    variables: {}

モデルの定義

TypeSpec では モデル を使ってデータの構造を定義します。モデルは TypeScript のインターフェースと似たようなレコードの形式または配列です。

main.tsp
model User {
  id: string;
  name: string;
}

モデルはコンパイルされると components/schemas として OpenAPI のスキーマに変換されます。

openapi.yaml
components:
  schemas:
    User:
      type: object
      required:
        - id
        - name
      properties:
        id:
          type: string
        name:
          type: string

ビルトインで用意されているプリミティブな型は Built-in Types を参照してください。別のモデルをデータ型として使うこともできます。

main.tsp
model User {
  id: string;
  name: string;
  address: Address;
}
 
model Address {
  street: string;
  city: string;
  state: string;
  zip: string;
}

オプショナルなプロパティを定義するには ? を使います。

main.tsp
model User {
  id: string;
  name: string;
  birthday?: utcDateTime;
  address: Address;
}

オプショナルなプロパティにはデフォルト値を指定できます。

main.tsp
model User {
  id: string;
  name: string;
  birthday?: utcDateTime = "2000-01-01T00:00:00Z";
  address: Address;
}

ビルトインのデコレーターを使うことで、プロパティに対してバリデーションを定義できます。

main.tsp
model User {
  id: string;
 
  @minLength(1)
  @maxLength(10)
  name: string;
 
  birthday?: utcDateTime;
  address: Address;
}

@doc デコレーターはモデルやプロパティに対してドキュメントを定義します。

main.tsp
@doc("ユーザー")
model User {
  id: string;
 
  @doc("ユーザー名")
  @minLength(1)
  @maxLength(10)
  name: string;
 
  birthday?: utcDateTime;
  address: Address;
}

またはドキュメントコメント(/** ... */)を使うこともできます。

main.tsp
/**
 * ユーザー
 */
model User {
  id: string;
 
  /**
   * ユーザー名
   */
  @minLength(1)
  @maxLength(10)
  name: string;
 
  birthday?: utcDateTime;
  address: Address;
}

スプレッド演算子(...)を使うことで、他のモデルのプロパティをコンポジションできます。

main.tsp
model User {
  id: string;
  name: string;
  birthday?: utcDateTime = "2000-01-01T00:00:00Z";
  address: Address;
}
 
enum Role {
  read,
  write,
}
 
model Admin {
  ...User;
  role: Role;
}

extends キーワードを使うことで、他のモデルを継承できます。これは明示的に関係を表したい時に使います。

main.tsp
model User {
  id: string;
  name: string;
  birthday?: utcDateTime = "2000-01-01T00:00:00Z";
  address: Address;
}
 
enum Role {
  read,
  write,
}
 
model Admin extends User {
  role: Role;
}

Template を使うことで、特定のパターンのデータ型を簡単に定義できます。例えば、ページネーションのレスポンスを表す Pagination モデルを定義してみましょう。

main.tsp
model Pagination<T> {
  items: T[];
  total: int32;
  offset: int32;
  limit: int32;
  count: int32;
}

この Pagination モデルは、T という型パラメーターを持っています。この型パラメーターは Pagination モデルを使う時に実際の型に置き換えられます。

main.tsp
model UserPagination extends Pagination<User> {}
// => {
//   items: User[];
//   total: int32;
//   offset: int32;
//   limit: int32;
//   count: int32;
// }

リソースの定義

リソースは REST API のエンドポイントを表します。リソースは @route デコレーターを使って定義します。@route デコレーターはパス名を引数に取ります。

main.tsp
@route("/users")
interface Users {}

interface のメソッドとしてエンドポイントに対する操作を定義します。リクエストパラメータをメソッドの引数に、レスポンスをメソッドの戻り値として定義します。

main.tsp
@error
model Error {
  code: int32;
  message: string;
}
 
@route("/users")
interface Users {
  // @query デコレーターはクエリパラメータ
  list(@query limit: int32, @query skip: int32): Pagination<User>;
  // @body デコレーターはリクエストボディ
  create(@body user: User): User | Error;
  // @path デコレーターはパスパラメータ
  get(@path id: string): User | Error;
}

このコードをコンパイルすると以下のように変換されます。

openapi.yaml
paths:
  /users:
    get:
      operationId: Users_list
      parameters:
        - name: limit
          in: query
          required: true
          schema:
            type: integer
            format: int32
        - name: skip
          in: query
          required: true
          schema:
            type: integer
            format: int32
      responses:
        '200':
          description: The request has succeeded.
          content:
            application/json:
              schema:
                type: object
                required:
                  - items
                  - total
                  - offset
                  - limit
                  - count
                properties:
                  items:
                    type: array
                    items:
                      $ref: '#/components/schemas/User'
                  total:
                    type: integer
                    format: int32
                  offset:
                    type: integer
                    format: int32
                  limit:
                    type: integer
                    format: int32
                  count:
                    type: integer
                    format: int32
    post:
      operationId: Users_create
      parameters: []
      responses:
        '200':
          description: The request has succeeded.
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/User'
                  - $ref: '#/components/schemas/Error'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/User'
  /users/{id}:
    get:
      operationId: Users_get
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The request has succeeded.
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/User'
                  - $ref: '#/components/schemas/Error'

デコレーターにより HTTP メソッドを指定しない場合にはデフォルトで GET メソッドとなり、@body デコレーターが指定されている場合に限り POST メソッドとして扱われます。

それぞれの HTTP メソッドに対応するデコレーターを使うことで、明示的に HTTP メソッドを指定できます。

main.tsp
@route("/users")
interface Users {
  @get
  list(@query limit: int32, @query skip: int32): Pagination<User>;
 
  @post
  create(@body user: User): User | Error;
 
  @get
  get(@path id: string): User | Error;
 
  @put
  update(@path id: string, @body user: User): User | Error;
 
  @delete
  delete(@path id: string): User | Error;
}

@header デコレーターを使うとヘッダーを、statusCodes デコレーターを使うとステータスコードそれぞれ定義できます。

main.tsp
@route("/users")
interface Users {
  @get
  list(@query limit: int32, @query skip: int32, @header ifMatch?: string): {
    @header ETag: string;
    @body pagenationUser: Pagination<User>;
  };
 
  @post
  create(@body user: User): {
    @statusCode statusCode: 201 | 400;
    @body User: User | Error;
  };
}

まとめ

  • TypeSpec は TypeScript にインスパイアされた言語で、開発者が親しみやすい構文で OpenAPI のスキーマを定義できる
  • モデルを使ってデータの構造を定義する
  • @route デコレーターを使って REST API のエンドポイントを定義する

参考