TypeSpecを使ってOpenAPIを生成しよう
こんにちは。
リーナー開発チームのめろたんです。
最近は、年始から肘を脱臼と骨折をやりました。
みなさんは肘を大事にしてください。
今回は、TypeSpecを使ってAPIのスキーマを定義してる話をします!
TypeSpec is なに
TypeSpecは、Microsoftが開発している、APIのスキーマを定義するためのツール・記述フォーマットです。
APIスキーマを定義するものだとOpenAPIが広く知られていますが、要は同じことができるものになります。
簡単にどういうものかを例を見ていきましょう。
import "@typespec/http";
using Http;
model Store {
name: string;
address: Address;
}
model Address {
street: string;
city: string;
}
@route("/stores")
interface Stores {
list(@query filter: string): Store[];
read(@path id: Store): Store;
}
このような感じで記述していきます。
Webフロントエンダーな皆様につきましては、TypeScriptのような記述形式で書きやすく読みやすいかなと思います。
ただこれだと、実際にスキーマとして使うことが現状だと難しいので、TypeSpecにはOpenAPIの形式に出力する機能があります。
実際にこれをOpenAPIに変換すると以下のようになります。
openapi: 3.0.0
info:
title: (title)
version: 0.0.0
tags: []
paths:
/stores:
get:
operationId: Stores_list
parameters:
- name: filter
in: query
required: true
schema:
type: string
responses:
'200':
description: The request has succeeded.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Store'
/stores/{id}:
get:
operationId: Stores_read
parameters:
- name: id
in: path
required: true
schema:
$ref: '#/components/schemas/Store'
responses:
'200':
description: The request has succeeded.
content:
application/json:
schema:
$ref: '#/components/schemas/Store'
components:
schemas:
Address:
type: object
required:
- street
- city
properties:
street:
type: string
city:
type: string
Store:
type: object
required:
- name
- address
properties:
name:
type: string
address:
$ref: '#/components/schemas/Address'
こんな感じで、OpenAPIを素で書くより圧倒的に書きやすく読みやすい形式で記述できるのがTypeSpecの利点になるかなと思います。
実際にどういう感じでつかっているか
大前提として、弊社はB2Bのサービスを作っているため、契約社様とその先の取引先様で、それぞれアクセスできるAPIのエンドポイントが違います。
そのために少し工夫している点があるので、それについて書いていきます!
ネームスペースを利用
契約社様とその先の取引先様で、それぞれアクセスできるAPIのエンドポイントが違うため、それぞれにネームスペースを作って、整理しています。
@route("/api")
namespace App {
// 契約社様用のAPIのエンドポイント
@route("/contract_companies")
namespace ContractCompanyAPI
// 取引先様用のAPIのエンドポイント
@route("/partner/{name}")
namespace PartnerAPI
}
このようにすることで、契約企業様用のAPIは/api/contract_companies
で、取引先様用のAPIは/api/partner/{name}
とすることができます。
そしてそれぞれのネームスペース配下にAPIを定義していくことで、それぞれ専用にAPIを記述することができます。
ファイルを分割して、各APIを定義
TypeSpecではimport
を使うことで、別ファイルに定義した内容を取り込むことができます。
そのため、各API毎にファイルを分割して、見通しが良くなるように作っています。
import "./data_api.tsp"
import "./user_api.tsp"
model DataListResponse {
data: Data[];
}
model DataShowResponse {
data: Data;
}
namespace App.ContractCompanyAPI {
@routes("/data")
interface DataAPI {
@get list(): DataListResponse | Error;
@get show(@path id: string): DataShowResponse | Error;
}
}
このようにすることで、契約企業様用のAPIは/api/contract_companies
で、取引先様用のAPIは/api/partner/{name}
とすることができます。
そしてそれぞれのネームスペース配下にAPIを定義していくことで、それぞれ専用にAPIを記述することができます。
モデルの共通化
先述した通り、契約社様とその先の取引先様で、それぞれアクセスできるAPIのエンドポイントが違うのですが、とはいえ扱うデータの型は一緒ということもあります。
その場合は、共通のエンティティとして別でmodel
を定義するようにしています。
model Data {
title: string;
description: string;
}
import "../common/entity.tsp"
model DataListResponse {
data: Data[];
}
// ....
import "../common/entity.tsp"
model DataListResponse {
data: Data[];
}
// ....
このようにすることで、共通のAPIであったり、内部的に同じデータを扱っている場合で、APIのレスポンスを変えたりした場合、一箇所を修正することで、簡単に対応することができます。
JSON Schemaの生成
TypeSpecでは、OpenAPI以外にJSON Schemaを生成することも可能です。
バックエンドで、より強固にバリデーションをかけたい等のシーンがある場合は、
困ったところ・難しかったところ
aliasの扱い
alias
というのがありまして、
model ModelA {}
// ....
alias Models = ModelA | ModelB | ModelC
のようにして、文字通りエイリアスを作ることができます。
TypeScript
でいうところのtype
と似たような感じですね。
そしてこのalias
で定義してOpenAPIを生成すると、Models
というcomponents
は生成されず、このModels
を使っている各所でこの内容が展開されます。
そのため、コードを書く際にModels
型がないため、扱いがちょっとめんどくさくなってしまいました。
// Modelsを受け取って何かしら処理したいが、Modelsというcomponentsは無いため、OpenAPIからコード生成してもModelsという型ができず、自分たちで定義し直す必要がある。
type Models = ModelA | ModelB | ModelC;
funtion f(models: Models) {
}
こうすると、Models
に新たにModelD
を追加した際に、自分たちでまた型を修正する必要があり、バグの温床になるなという感じがありました。
そのため、alias
を使わず、union
を使うように変更しました。
model ModelA {}
// ....
union Models {
ModelA;
ModelB;
ModelC;
}
このようにすることで、components
にModels
が生成されるようになり、コード生成する際も型が定義されるようになるため、自分たちでプラスでメンテするところを減らせました。
共通のパラメーターの定義
先述した、ネームスペースを利用して、その@route
にurlパラメーターをつけているところがあります。
そのネームスペース配下に定義した各APIに都度@path
でパラメーターをすべて付ける必要があり、それを都度書くのがめんどくさく、冗長に感じてちょっとやだなあという感じがあります。
@route("/partner/{name}"
namespace App.PartnerAPI {
@route("/data/")
interface DataAPI {
@get list(@path name: string) DataListResponse | Error;
@get show(@path name: string, @path uid: string) DataShowResponse | Error;
}
}
このような場合、すべてのAPIに@path name: string
を都度書く必要があり、めんどうだなとなっていました。
そのため、
model CommonParameters {
@path name: string;
}
// ....
@get list(...CommonParameters) DataListResponse | Error;
@get show(...CommonParameters, @path uid: string) DataShowResponse | Error;
このように変更して、多少楽にすることができたのですが、それでもCommonParameters
を都度各必要があり、ちょっとめんどくさいのは残ったままだなぁという感じになっています。
TypeSpecは自分たちでDecorator
を作ることができるため、これでうまくできるのではと考えていますが、まだ取り組めていません。
まとめ
OpenAPIを素で書くより、圧倒的に書きやすく読みやすい形でつくれるのはとても良いなと思います!
またOpenAPIからコード生成するツール等がたくさんあるので、TypeSpecからOpenAPIを生成できるようにしているのは、現実的でとても扱いやすいなと感じています。
宣伝
リーナーではTypeSpecでAPIのスキーマを書いていきたいエンジニアを募集しています!
Discussion