typed-api-specの機能
API定義
ApiEndpoints
DefineApiEndpointsを利用することで、API定義を行うことができます
code: api-endpoints.ts
export type PathMap = DefineApiEndpoints<{
"/users": {
get: {
resBody: {
// Status Codeごとにレスポンスを定義する
200: { userNames: string[] };
400: { errorMessage: string };
};
};
};
}>;
const main = async () => {
const fetchT = fetch as FetchT<"", PathMap>;
const res = await fetchT("/users");
if (!res.ok) {
// !res.okなので、eはStatus Code:400である{ errorMessage: string }に推論される
const e = await res.json();
return console.log(e.errorMessage);
}
// res.okなので、rはStatus Code:200である{ userNames: string[] }に推論される
const r = await res.json();
console.log(r.userNames);
};
main();
Query Parameter, Path Variable, Bodyなども定義できます
code: api-endpoints-schema.ts
export type PathMap = DefineApiEndpoints<{
"/users/:userId": {
// HTTP Methodを指定する。get、post,put,deleteなど
get: {
// Query Parameter
query: { detail: boolean }
// Path Variable
params: { userId: string }
resBody: {...};
};
};
"/users": {
post: {
// HTTP Request Body
body: { userName: string }
resBody: { 200: { userNames: string[] } };
}
}
}>;
ZodApiEndpoints
ZodApiEndpointsをsatisfiesすることで、スキーマの型チェックを行うことができます
code: zod-api-endpoints.ts
export const pathMap = {
"/users": {
get: {
resBody: {
200: z.object({ userNames: z.string().array() }),
400: z.object({ errorMessage: z.string() }),
},
}
} satisfies ZodApiEndpoints;
anyZ
ZodApiEndpointsで純粋なTypeScriptの型定義を利用したい場合、anyZを利用することができます。
anyZを利用した場合、API定義としては指定した型を利用しつつ、バリデーション時は何も行いません
実装は単にanyZ = <T>() => z.any() as ZodType<T>としているだけです
code: zod-api-endpoints-anyz.ts
type UsersResponse = { userNames: string[] }
export const pathMap = {
"/users": {
get: {
resBody: {
200: anyZ<UsersResponse>(),
400: z.object({ errorMessage: z.string() }),
},
}
} satisfies ZodApiEndpoints;
fetch
型定義の付与
window.fetchにFetchTの型定義を適用することで、API定義を利用したより厳密な型チェックを実行できます
例えばhttps://example.com/base_path/とhttps://example.com/base_path/usersの2つのエンドポイントを持つ場合は以下のようにします
code: fetch.ts
export type PathMap = DefineApiEndpoints<{
"/": {...};
"/users": {...};
}>;
パスのチェック
ApiEndpointsで定義したパス以外を渡すと型エラーになります
code: fetch-path-check.ts
https://nota.gyazo.com/04af55a84f27296091f9953c491ad203
Status CodeによるResponseの絞り込み
res.okの場合、レスポンスは20XのステータスコードであるレスポンスのUnion型になります
!res.okの場合、レスポンスは20X以外のステータスコードであるレスポンスのUnion型になります
switch(res.status)を利用することで、より厳密なレスポンス型の絞り込みが可能です
code: api-endpoints.ts
const main = async () => {
const fetchT = fetch as FetchT<"", PathMap>;
const res = await fetchT("/users");
switch(res.status): {
case 200:
// rはStatus Code:200である{ userNames: string[] }に推論される
const r = await res.json();
console.log(r.userNames);
case 400:
// eはStatus Code:400である{ errorMessage: string }に推論される
const e = await res.json();
return console.log(e.errorMessage);
}
};
main();
https://nota.gyazo.com/abaf8ee3f82c8a31d1147fdf64e73e4d
body
bodyとしてjsonを受け取るエンドポイントでは、そのjsonのスキーマをAPI定義として表現することができます
fetch側でbodyに値を渡す際には、typed-api-spec/jsonで提供しているJSON型を利用してください
code: post-body.ts
export type PathMap = DefineApiEndpoints<{
"/users": {
post: {
body: { userName: string }
res: { 200: { userNames: string[] } };
};
};
}>;
import JSONT from @mpppk/typed-api-spec/json
const JSONT = JSON as JSONT
const fetchT = fetch as FetchT<"", PathMap>;
const res = await fetchT("/users", {
method: 'post',
// Use JSONT instead of JSON
body: JSONT.stringify({userName: "niboshi"})
});
https://nota.gyazo.com/4c6c94ee2ae31cbe3163cc2852e29f91
Express
code: express.ts
const pathMap = {
"/users": {
get: {
query: z.object({ page: z.string() }),
resBody: {
200: z.object({ userNames: z.string().array() }),
400: z.object({ errorMessage: z.string() }),
},
}
} satisfies ZodApiEndpoints;
const app = express();
app.use(express.json());
const wApp = typed(pathMap, app);
wApp.get("/users", (req, res) => {
// API定義したquery parameterに基づいたバリデーションを実行
const r = res.locals.validate(req).query();
if (r.success) {
// res.jsonはStatus Code:200のレスポンス型のみを受け入れる
res.json({ userNames: [page${r.data.page}#user1] });
} else {
// res.status(400).jsonはStatus Code:400のレスポンス型のみを受け入れる
res.status(400).json({ errorMessage: r.error.toString() });
}
});
typed
const wApp = typed(pathMap, express());
与えられたExpress Routerに対して以下の2つを行うメソッドです
API定義に基づいたバリデーションを実行するreq.locals.validateを追加するミドルウェアを適用
Express Routerに、API定義を利用したより厳密な型を付与
戻り値のRouterは、引数として与えたExpress Routerと型定義以外は同じものです
asAsync
Expressは、非同期処理中に発生したPromise.rejectや例外のthrowはユーザが明示的にハンドリングする必要があります
asAsyncはこのような例外ハンドリングを自動的に行うExpress Routerのラッパーを返します
code: as-async.ts
const wApp = asAsync(typed(pathMap, express()));
wApp.get("/users", async (req, res) => {
// asAsyncによってラップされたExpress Routerは、asyncハンドラからthrowされた例外をExpressエラーハンドラへ渡してくれる
throw Error()
});
Status CodeによるResponseの絞り込み
上記コード例で紹介したように、typedメソッドで型付けされたExpress Routerでは、res.status(400)のように指定したステータスコードに応じて、レスポンスの型チェックを行います
Handler型
typedメソッドで型付けされたExpress Routerに直接ハンドラを定義することで、適切な型推論が行われます
Handlers型を利用することで、別途定義することが可能です
code: handlers.ts
type Handlers = ToHandlers<PathMap>
export const getUsers: Handlers'/users''get' = (req, res) => {... }