TypeSpecを使ってみた
はじめに
私は業務でAWSのSAM(Serverless Application Model)を使ってリソースを構築することが多いです。ある程度アプリケーションが大きくなっていくとyamlに定義したリソースが多くなり、変更したいリソースを探すのに苦労することがあります。
API定義などは特に肥大化しやすく、もう少し簡単に書けないものかと悩むことが多いです。
今回は代替の検討対象としてTypeSpecを触ってみたので、簡単にまとめたいと思います。
TypeSpecとは
TypeSpecはMicrosoftが開発しているAPI定義言語です。
REST, OpenAPI, gRPCと一通りの定義が可能です。
独自拡張子(.tsp
)を使用し、多くの定義がデコレータを使って定義します。
デコレータが追加されたTypeScriptライクのAPI定義という感じです。
使ってみる
プロジェクト作成
nodejsで以下のコマンドを実行し、TypeSpecのプロジェクトを作成します。
npx tsp init
実行すると以下の質問が表示されます。
- プロジェクトフォルダを初期化するか (フォルダに既にプロジェクトがある場合のみ)
- テンプレート
- 今回はREST API定義なので2番目の
Generic REST API
- 今回はREST API定義なので2番目の
- プロジェクト名
- ライブラリ
-
@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: 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