🐥

TypeSpecを使ってみた

2024/07/18に公開

はじめに

私は業務でAWSのSAM(Serverless Application Model)を使ってリソースを構築することが多いです。ある程度アプリケーションが大きくなっていくとyamlに定義したリソースが多くなり、変更したいリソースを探すのに苦労することがあります。

API定義などは特に肥大化しやすく、もう少し簡単に書けないものかと悩むことが多いです。
今回は代替の検討対象としてTypeSpecを触ってみたので、簡単にまとめたいと思います。

TypeSpecとは

https://typespec.io/
TypeSpecはMicrosoftが開発しているAPI定義言語です。
REST, OpenAPI, gRPCと一通りの定義が可能です。

独自拡張子(.tsp)を使用し、多くの定義がデコレータを使って定義します。
デコレータが追加されたTypeScriptライクのAPI定義という感じです。

使ってみる

プロジェクト作成

nodejsで以下のコマンドを実行し、TypeSpecのプロジェクトを作成します。

npx tsp init 

実行すると以下の質問が表示されます。

  • プロジェクトフォルダを初期化するか (フォルダに既にプロジェクトがある場合のみ)
  • テンプレート
    • 今回はREST API定義なので2番目のGeneric REST API
  • プロジェクト名
  • ライブラリ
    • @typespec/openapi3だけはOpenAPI定義の細かい設定をしないなら不要です。

作成が完了すると以下のファイルが作成されます。

package.json      
tspconfig.yaml # TypeSpecの設定ファイル
main.tsp       # エントリーポイント(定義ファイル)

API定義

今回はOpenAPIのサンプルであるPetStoreのうち、petタグが付与されている部分をTypeSpecで記述してみます。

import "@typespec/http";
import "@typespec/rest";
import "@typespec/openapi3";

using TypeSpec.Rest;
using TypeSpec.Http;

@service({
  title: "Pet Store",
})
namespace PetStore;

model Pet {
  id: integer;
  name: string;
  category: {
    od: integer;
    name: string;
  };
  photoUrls: string[];
  tags: {
    id: integer;
    name: string;
  };
  status: "available" | "unavailable";
}

model Empty {}

@tag("pet")
@route("/pet")
interface Pets {
  @post
  registPet(@body body: Pet): Pet;
  @put
  updatePet(@body body: Pet): Pet;

  @route("/findByStatus")
  @get
  findByStatus(@query status: string): Pet;

  @route("/findByTags")
  @get
  findByTags(
    @query({
      format: "multi",
    })
    status: string[],
  ): Pet;
}

@tag("pet")
@route("/pet/{petId}")
namespace PetById {
  model Path {
    @path
    petId: string;
  }

  @get
  op findById(...Path): Pet;

  @post
  op updatePetById(...Path, @query name: string, @query status: string): Empty;

  @delete
  op deletePet(...Path): Empty;

  @route("/uploadImage")
  op uploadImage(...Path, @query addtionalMetadata: string): {
    code: integer;
    type: string;
    message: string;
  };
}

service

openapiのinfoタグの内容を記述できます。
typespec/openapi3にも@infoというデコレータが存在しますが、titleとversion以外の定義が必要な場合はそちらを使用するのが良いと思います。

@service({
  title: "Pet Store",
})

// service宣言時はファイルレベルのnamespaceが必要
namespace PetStore;

model

データスキーマを定義します。openapiでいえばcomponentsと同じです。
TypeScripのようにスプレッド構文やジェネリクスも利用可能なため、汎用性は高いと思います。

model Pet {
  id: integer;
  name: string;
  category: {
    od: integer;
    name: string;
  };
  photoUrls: string[];
  tags: {
    id: integer;
    name: string;
  };
  status: "available" | "unavailable";
}

operation

エンドポイントに紐づく操作を定義し、パラメータ、レスポンス、識別子となる名前を宣言します。@typespec/httpにより利用可能なデコレータを利用することでHTTPメソッドやパスをわかりやすく定義可能です。

@post
registPet(@body body: Pet): Pet;

@put
updatePet(@body body: Pet): Pet;

@route("/findByStatus")
@get
findByStatus(@query status: string): Pet;

namespace

namespaceは様々な型定義をグループ化することができます。
model, operation, namespaceなどtypespecで定義可能な型はほとんど含められます。

@tag("pet")
@route("/pet/{petId}")
namespace PetById {
  model Path {
    @path
    petId: string;
  }

  @get
  op findById(...Path): Pet;

  @post
  op updatePetById(...Path, @query name: string, @query status: string): Empty;

  @delete
  op deletePet(...Path): Empty;

  @route("/uploadImage")
  op uploadImage(...Path, @query addtionalMetadata: string): {
    code: integer;
    type: string;
    message: string;
  };
}

interface

interfaceはoperationをグループ化するために使用します。
operationのみが定義可能で、namespaceでの記述では必要だったopが不要となります。

@tag("pet")
@route("/pet")
interface Pets {
  @post
  registPet(@body body: Pet): Pet;
  @put
  updatePet(@body body: Pet): Pet;

  @route("/findByStatus")
  @get
  findByStatus(@query status: string): Pet;

  @route("/pet/findByTags")
  @get
  findByTags(
    @query({
      format: "multi",
    })
    status: string[],
  ): Pet;
}

出力

OpenAPIの出力には以下のコマンドを実行します。

tsp compile .

# ディレクトリを指定する場合
tsp compile . --output-dir 出力するディレクトリ

# ファイル名を指定する場合
tsp compile . --options @typespec/openapi3.output-file=ファイル名

# ファイル名を指定する場合
tsp compile . --options @typespec/openapi3.output-file=ファイル名 

デフォルトではtsp-output/@typespec/openapi3/openapi.yamlで出力されます。
出力先は{output-dir}/{emitter名}/{ファイル名}の形式で決まり、emitter名の箇所は変更不可です。

出力したファイル
openapi.yaml
openapi: 3.0.0
info:
  title: Pet Store
  version: 0.0.0
tags:
  - name: pet
paths:
  /pet:
    post:
      tags:
        - pet
      operationId: Pets_registPet
      parameters: []
      responses:
        '200':
          description: The request has succeeded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Pet'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Pet'
    put:
      tags:
        - pet
      operationId: Pets_updatePet
      parameters: []
      responses:
        '200':
          description: The request has succeeded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Pet'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Pet'
  /pet/findByStatus:
    get:
      tags:
        - pet
      operationId: Pets_findByStatus
      parameters:
        - name: status
          in: query
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The request has succeeded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Pet'
  /pet/findByTags:
    get:
      tags:
        - pet
      operationId: Pets_findByTags
      parameters:
        - name: status
          in: query
          required: true
          schema:
            type: array
            items:
              type: string
          style: form
          explode: true
      responses:
        '200':
          description: The request has succeeded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Pet'
  /pet/{petId}:
    get:
      tags:
        - pet
      operationId: PetById_findById
      parameters:
        - $ref: '#/components/parameters/PetById.Path'
      responses:
        '200':
          description: The request has succeeded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Pet'
    post:
      tags:
        - pet
      operationId: PetById_updatePetById
      parameters:
        - $ref: '#/components/parameters/PetById.Path'
        - name: name
          in: query
          required: true
          schema:
            type: string
        - name: status
          in: query
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The request has succeeded.
    delete:
      tags:
        - pet
      operationId: PetById_deletePet
      parameters:
        - $ref: '#/components/parameters/PetById.Path'
      responses:
        '200':
          description: The request has succeeded.
  /pet/{petId}/uploadImage:
    get:
      tags:
        - pet
      operationId: PetById_uploadImage
      parameters:
        - $ref: '#/components/parameters/PetById.Path'
        - name: addtionalMetadata
          in: query
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The request has succeeded.
          content:
            application/json:
              schema:
                type: object
                properties:
                  code:
                    type: integer
                  type:
                    type: string
                  message:
                    type: string
                required:
                  - code
                  - type
                  - message
components:
  parameters:
    PetById.Path:
      name: petId
      in: path
      required: true
      schema:
        type: string
  schemas:
    Pet:
      type: object
      required:
        - id
        - name
        - category
        - photoUrls
        - tags
        - status
      properties:
        id:
          type: integer
        name:
          type: string
        category:
          type: object
          properties:
            od:
              type: integer
            name:
              type: string
          required:
            - od
            - name
        photoUrls:
          type: array
          items:
            type: string
        tags:
          type: object
          properties:
            id:
              type: integer
            name:
              type: string
          required:
            - id
            - name
        status:
          type: string
          enum:
            - available
            - unavailable

まとめ

今回はAPI定義言語のTypeSpecの紹介でした。
個人的に可読性がかなり高く、typescriptのaws-cdkと一緒に使用するとよさそうだなと感じました。nodejsの使用に躊躇がないプロジェクトなら導入を検討してもよいかもしれません。

今回紹介できなかった機能も、調べて記事にしていけたらと思います。

Discussion