Cloudflare Workersで手軽にRESTful APIを公開する

今回は、OpenAPI Specification から良い感じのドキュメントサイトを提供してくれるサービス bump.sh を見つけたので、RESTful API を用意して試してみます。

ドキュメントサイトがあることで、API が公開されていることがより分かりやすくなるでしょう。

こちらはスキーマの差分を解析するツール API Diff · Powered by Bump.sh を提供しており、今後 GraphQL もサポートされるそうなので楽しみです。

RESTful API

まずは JSON を返すだけの簡単なAPIを作りたいと思います。 軽量なアプリケーションとなるので Cloudflare Workers をデプロイ先とします。

手書きしたくない

ゼロから書くので定義ファイルとサーバーサイドの実装どちらも手書きすることはなるべく避けたいところです。

Node.js では path 毎に対応するスキーマの定義をコロケーション的に記述できる express-openapi や定義ファイルからサーバのコードを生成出来る OpenAPITools/openapi-generator の nodejs-express-server などがありそうでした。

Cloudflare Workers

では Cloudflare Workers に適したものはあるのでしょうか?

Cloudflare Workers でも動作するフレームワークといえば honojs/hono: Ultrafast web framework for the Edge があります。

OpenAPI and SwaggerUI support · Issue #717 · honojs/hono によると hono は今の所 OpenAPI についてサポートはしていないようですが、代わりに cloudflare/itty-router-openapi が紹介されていました。

今回はこちらを使ってみることにしましたが、他にも良さそうなものがあれば教えていただけると幸いです。

cloudflare/itty-router-openapi

github.com

cloudflare/itty-router-openapi を使うとコードベースから定義ファイルを生成できる RESTful API のアプリケーションを作ることが出来ます。 express-openapiと似たように、path 毎に対応するスキーマの定義をおこなっていく方法のようです。

当社の開発方法:Cloudflare Radar 2.0を支える技術 で紹介されており、実際の製品に使われているところも安心して使えそうです。

実装

github.com

今回作ってみたものを例にすると以下のような雰囲気の実装になります。

パラメータやレスポンスをコロケーション的に記述出来るのは、実装していく際に何を受け取って何を返すべきかが明確になって良さそうでした。

const router = OpenAPIRouter()
class getSeason extends OpenAPIRoute {
  static schema = {
    parameters: {
      season: Path(Number, {
        description: 'Season number',
        required: true,
      }),
    },
    responses: {
      '200': { schema: { season: SeasonSchema } },
      '404': {
        description: 'Not found season',
        schema: { message: new Str({ example: 'Not found season' }) },
      },
    },
  }
  async handle(props: HandleProps) {
    const seasonNumber = props.params['season']
    if (!seasonNumber) return newResponse({ message: 'Not found season' }, 404)

    const season: T | undefined = data[Number(seasonNumber)]
    if (!season) return newResponse({ message: 'Not found season' }, 404)
    return newResponse({ season })
  }
}
router.get(`/xxx`, getSeason)

テスト

itty-router-openapi では定義したスキーマのレスポンスと handle メソッドが返す値の型があっているかどうかを確認することはできません。

そこで jest-openapi を使うと戻り値がOpenAPIドキュメントに沿っているかテストすることが出来ます。

なぜか素朴な Response 型には対応していなかったので、手で温かみのあるレスポンスを用意してあげましょう。

const dummyRequest = async (path: string) => {
  const target = `${SERVER_URL}/${path}`
  const request = new Request(target, {
    method: 'GET',
  })
  const response: Response = await router.handle(request)
  return {
    status: response.status,
    req: { path: target, method: request.method },
    body: await response.json(),
  }
}
const res = await dummyRequest(`/current`)
expect(res.status).toEqual(200)
expect(res).toSatisfyApiSpec()

OpenAPI Specification

ハンドラを登録した OpenAPIRouter の schema を JSON ファイルとして書き出すスクリプトを用意するだけで定義ファイルを生成することができます。

const router = OpenAPIRouter()
router.get(`/xxx`, OpenAPIRoute)
fs.writeFileSync('./public-api.json', JSON.stringify(router.schema, null, 2))

Bump.sh

ようやく?当初の目的である bump.sh を使うことが出来ます。

先程生成した定義ファイルをアップロードすることで以下のようなページを用意してくれました。Swagger UIと比べてもモダンで良い感じです。

bump-sh/github-action: GitHub action to deploy your API documentation on Bump

GitHub Actions もサポートされているので、デプロイと同時にドキュメントを自動更新することが出来て便利です。

https://bump.sh/kasu/doc/game-season-api

Better Uptime

Public Status Page | Better Uptime

JSON を返すだけなので落ちることはなさそうですが、せっかくなので Better Uptime を使ってかっこいいステータスページも用意しておきましょう。

任意のエンドポイントとそのリクエストを指定すると定期的にヘルスチェックを行ってくれます。

https://game-season-api.betteruptime.com/