天下一APIスキーマ管理武道会
https://gyazo.com/7aa7526e77711427ae2528e2493a18d4
自己紹介
株式会社Helpfeel
APIスキーマ管理とは
APIのリクエストやレスポンス、ヘッダの構造、エンドポイントなどを記述したスキーマを作成し、それを使ってAPIを開発・テスト・ドキュメント化・保守すること
特にクライアント/サーバそれぞれでスキーマを利用することで、整合性を担保することができる
武道会開催の背景(Helpfeelの状況)
REST APIが既にある程度存在しており、OpenAPIを利用し、スキーマとコードは手動で同期(抜け漏れないよう両方を変更)している
コードを更新したけどスキーマを更新し忘れるなど、メンテが大変
よりメンテしやすい環境にするために、世間のAPIスキーマ管理アプローチを比較選定したい
HelpfeelはFull-Stack TypeScriptなので、それにマッチしたものが良い
(あくまで弊社の環境におけるベストなソリューションを探す目的であり、ライブラリの優劣をつける趣旨ではありません)
Full-Stack TypeScript環境でのAPIスキーマ管理に期待すること
TypeScriptの型システムとAPIスキーマの相互運用性
スキーマファースト: スキーマからTypeScriptのコードを生成する
コードファースト: TypeScriptのコードからスキーマを生成する
TypeScriptのコードそのものがスキーマであれば、これらを考える必要はない
ランタイムバリデーション
クライアント/サーバ間の通信では、信頼できない入力が与えられる可能性があるため、バリデーションは必須
スキーマ定義に従ってバリデーションされて欲しい
(Helpfeelでは)期待しないこと
スキーマを特定のプログラミング言語に依存させない
スキーマから複数プログラミング言語のクライアントを生成したい
例えばHelpfeel(プロダクト)ではネイティブアプリは提供していない
既存技術の比較
(基本的にエアプ机上調査です。認識違いなどあればご指摘ください🙇)
OpenAPI
yamlによるスキーマ定義
エコシステムが充実しており、スキーマファースト、コードファーストどちらも、様々な選択肢がある
code: openapi.yaml
openapi: 3.0.0
info:
title: Sample API
paths:
/users:
get:
summary: Returns a list of users.
responses:
"200": # status code
description: A JSON array of user names
content:
application/json:
schema: {type: array, items: {type: string}}
code: openapi-typescript.ts
import type { paths, components } from "./my-openapi-3-schema"; // generated by openapi-typescript
// Schema Obj
// Path params
// Response obj
type SuccessResponse =
type ErrorResponse =
frourio+aspida
TypeScriptによるAPIスキーマをベースにクライアント/サーバそれぞれで型推論やバリデーションを行うアプローチ
サーバ実装非依存(frourioを利用する場合はfastifyとExpressから選択可能)
https://gyazo.com/bbbd06a2fa5e06e9490ce2b57428126b
code: aspida.ts
import type { DefineMethods } from "aspida";
type User = { id: number; name: string; };
export type Methods = DefineMethods<{
get: { query?: { limit: number; }; resBody: User[]; };
post: { reqBody: { name: string; }; resBody: User; }
}>;
https://gyazo.com/b96d5c0b5890768c109ff41b9db8a4ea
Zodios
TypeScriptによるAPIスキーマをベースにクライアント/サーバそれぞれで型推論やバリデーションを行うアプローチ
サーバ実装非依存だが、ExpressとNext.js(page router)のインテグレーションが提供されている
code: zodios-def.ts
const apiClient = new Zodios('/api', [
{
method: "get", path: "/users", alias: "getUsers",
response: z.array(user),
},
{
method: "get", path: "/users/:id", alias: "getUser",
response: user,
},
{
method: "post", path: "/users", alias: "createUser",
response: user,
},
]);
// get all users
const users = await apiClient.getUsers();
// get user by id
const user = await apiClient.getUser({ params: { id: 1 } });
// create user
const newUser = await apiClient.createUser({ name: "John", age: 20, email: "jodn@doe.com"});
GraphQL
GraphQLスキーマからTypeScriptのコード生成を行うアプローチ
code: urql.ts
import React from 'react';
import { useQuery } from 'urql';
import './App.css';
import Film from './Film';
// ↓GraphQLスキーマから生成されたコード
import { graphql } from '../src/gql';
const allFilmsWithVariablesQueryDocument = graphql(`
query allFilmsWithVariablesQuery($first: Int!) {
allFilms(first: $first) {
edges {
node {
...FilmItem
}
}
}
}
`);
function App() {
// data is typed!
const data } = useQuery({
query: allFilmsWithVariablesQueryDocument,
variables: { first: 10 },
});
return (
<div className="App">
{data && (
<ul>
{data.allFilms?.edges?.map(
(e, i) => e?.node && <Film film={e?.node} key={film-${i}} />
)}
</ul>
)}
</div>
);
}
export default App;
tRPC
サーバ実装から生成した型定義だけをクライアント側で読み込むアプローチ
code: trpc-server.ts
import { publicProcedure, router } from './trpc';
const appRouter = router({
greeting: publicProcedure.query(() => 'hello tRPC v10!'),
});
// Export only the type of a router!
// This prevents us from importing server code on the client.
export type AppRouter = typeof appRouter;
code: trpc-client.ts
import { createTRPCClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from '../path/to/server/trpc';
const client = createTRPCClient<AppRouter>(...);
const bilbo = await client.getUser.query('id_bilbo');
// => { id: 'id_bilbo', name: 'Bilbo' };
Hono RPC
サーバ実装から生成した型定義だけをクライアント側で読み込むアプローチ
code: hono-server.ts
const route = app.post(
'/posts',
zValidator( 'form', z.object({ title: z.string(), body: z.string() }) ),
(c) => {
// ...
return c.json( { ok: true, message: 'Created!' }, 201 )
}
)
code: hono-client.ts
const res = await client.posts.$post({
form: { title: 'Hello', body: 'Hono is a cool project' }
})
React Server Actions
サーバ側で実行するメソッドをクライアントから透過的に呼び出すことができる
code: server-actions.ts
// Server Component
import Button from './Button';
function EmptyNote () {
async function createNoteAction() {
// Server Action
'use server';
await db.notes.create();
}
return <Button onClick={createNoteAction}/>;
}
アプローチのまとめ
スキーマ定義ファイルからTypeScriptコードを生成
openapi-typescript, GraphQL
TypeScriptでスキーマ定義
aspida, Zodios
サーバ実装から生成した型定義だけをクライアント側で読み込む
tRPC, Hono RPC
サーバ側で実行するメソッドをクライアントから透過的に呼び出し
Server Actions
Helpfeelではどれを選択すべきか
Helpfeelの技術選定のための要件
Full-Stack TypeScript環境なので、他のスキーマ定義ファイルなどではなくTypeScriptベースで定義したい
現行のREST APIから変更する場合は、相応の工数が必要
現行のサーバフレームワークであるExpressで利用可能
もう一つ、選定時に考えたこと
どの技術を選んだとしても、結局アプリケーションの細かい仕様に合わせたラッパーを書きたくなりそう
認証とか、APIのグルーピングとか、Rate Limitに引っかかった時のハンドリングとか
であれば、クライアントにリッチな機能は不要。バンドルサイズ的な観点でも薄ければ薄いほうが良い
まとめると、理想はこんなツール
REST APIに後付けできて、部分的に導入可能
TypeScriptだけでスキーマやバリデーションを定義できる
フレームワーク/バリデーションライブラリ非依存
可能な限り軽量
天下一APIスキーマ管理武闘会の勝者は…!
自社開発...!
なにィーーーー!!(画像略)
これまでの議論は何だったんだと思われるかもしれませんが、私が主催者なので、私がルールです
typed-api-spec
REST APIに後付けできて、部分的に導入可能
TypeScriptだけでスキーマやバリデーションを定義できる
フレームワーク/バリデーションライブラリ非依存
ゼロランタイム
使い方
1. TypeScriptでAPIスキーマ定義をする
code: spec.ts
type Spec = DefineApiEndpoints<{
"/users": { get: { responses: { 200: { body: { names: string[] } } } } };
}>;
2. ネイティブのfetchに型を付与する
code: client.ts
const fetchT = fetch as FetchT<"", Spec>;
const res = await fetchT("/users");
const data = await res.json(); // data is { names: string[] }
ゼロランタイム
fetchに型を付与するだけなので、ランタイムでは全く何もしない=ゼロランタイム
フレームワーク/バリデーションライブラリ非依存
typed-api-specのコア部分はフレームワークやバリデーションライブラリに依存していない
現時点ではフレームワークとしてExpressとFastify, バリデーションライブラリとしてzodとvalibotをサポートしている
レスポンスの型推論
HonoのResponse定義により、強力な推論が可能
今後の展望
自動バリデーションを行うfetchラッパー
ゼロランタイムなので、デフォルトではクライアントでのバリデーションは全く行わない。があると便利そう
import.metaを見て利用するかどうかを決定すれば、開発環境での自動バリデーション実行とプロダクションでのゼロランタイムを両立できるはず
OpenAPIスキーマのエクスポート
既存のエコシステムに何の努力もせず乗っかりたい
おわり
ありがとうございました
既存ツールでもそれはできるぞとかこういうアプローチのほうがいいんじゃないかなど、天下一懇親会でのバトルもお待ちしています!