HonoのHCで外部APIのクライアントを作る
HackerNewsのAPIのリクエストとレスポンスを型安全にする方法を考えます。少なくともAnyのまま扱うよりは安全な開発ができます。
https://github.com/HackerNews/API
リポジトリはこちらです。
https://github.com/RyukyuInteractive/blog-2024-08-27-hono-api-client
具体的にはHonoを用いてこのようにエンドポイントやそのリクエストとレスポンスを型安全にする試みです。
const client = hc<App>("https://hacker-news.firebaseio.com")
const resp = await client.v0.item[":item"].$get({
param: { item: "8863.json" },
query: { print: "pretty" },
})
const json = await resp.json()
if (json.type === "story") {
console.log(json)
}
このようにエンドポイントが補完されます。
URLの?print=pretty
の部分も補完されます。
HCを使ってみる
HCはHonoのRPCとなるライブラリです。
https://hono.dev/docs/guides/rpc
このようにHonoの型を渡すことで機能します。
import type { Hono } from "hono"
import { hc } from "hono/client"
import type { BlankEnv } from "hono/types"
import type { StatusCode } from "hono/utils/http-status"
type AppType = Hono<
BlankEnv,
{
"/v0/item/:item": {
$get: {
input: {
param: {
item: string
}
query: {
print?: "pretty"
}
}
output: {
type: "story" | "comment" | "job" | "poll" | "pollopt"
}
outputFormat: "json"
status: StatusCode
}
}
}
>
const client = hc<AppType>("https://hacker-news.firebaseio.com")
ここでhono
は型のみ使用しています。
import type { Hono } from "hono"
バリデーションを実装する
外部APIの仕様が不十分な場合は、ランタイムでバリデーションを実装することが出来ます。
import { vValidator } from "@hono/valibot-validator"
import { Hono } from "hono"
import { hc } from "hono/client"
import { literal, object, optional, parse } from "valibot"
import { vItem } from "~/models/item"
const hono = new Hono()
const app = hono.get(
"/v0/item/:item",
vValidator("query", object({ print: optional(literal("pretty")) })),
async (c) => {
const resp = await fetch(c.req.url, {
headers: { "Content-Type": "application/json" },
})
const json = await resp.json()
const result = parse(vItem, json)
return c.json(result)
},
)
const client = hc<typeof app>("https://hacker-news.firebaseio.com", {
fetch: app.request,
})
const resp = await client.v0.item[":item"].$get({
param: { item: "8863.json" },
query: { print: "pretty" },
})
const json = await resp.json()
if (json.type === "story") {
console.log(json)
}
ここで関数hc
にfetch
を渡してあげる必要があります。
const client = hc<typeof app>("https://hacker-news.firebaseio.com", {
fetch: app.request,
})
また、Honoは型ではなく実装が必要になります。
import { Hono } from "hono"
リクエストの型を定義する
ここでは@hono/valibot-validator
というライブラリを用いてバリデーションを実装しています。
https://hono.dev/docs/guides/validation
このように定義した場合は?print=pretty
の部分の方がバリデーションされます。
import { vValidator } from "@hono/valibot-validator"
import { literal, object, optional, parse } from "valibot"
vValidator("query", object({ print: optional(literal("pretty")) })),
ここでheadersはバリデーションせずにそのまま渡していますが、headersにトークンなどが含まれる場合はそれもバリデーションすることができます。
const resp = await fetch(c.req.url, {
headers: { "Content-Type": "application/json" },
})
このように続けてHeadersやBodyなどのバリデーションも追加できます。
const app = hono.get(
"/v0/item/:item",
vValidator("query", object({ print: optional(literal("pretty")) })),
vValidator("header", object({ token: string() })),
vValidator("json", object({ a: string() })),
async (c) => {}
)
レスポンスの型を定義する
返り値はValibotのparseでバリデーションを実行しています。予期しないレスポンスが返ってきた場合はここでエラーになります。
const result = parse(vItem, json)
HackerNewsのAPIのCommentやJobなど色々なレスポンスの値があります。
export const vItem = union([
vItemComment,
vItemJob,
vItemPoll,
vItemPollopt,
vItemStory,
])
例えば、Storyはこのようなバリデーションを定義できます。
import { array, literal, number, object, optional, string } from "valibot"
export const vItemStory = object({
id: number(),
type: literal("story"),
by: string(),
descendants: number(),
kids: array(number()),
score: number(),
text: optional(string()),
time: number(),
title: string(),
})
その他に
今回はHonoを使用しましたが、そのような目的で作られたライブラリがあります。
型定義を生成できずAPIクライアントのライブラリも存在しない外部のAPIを取り扱うことはよくあります。
そのような場合は、型補完やバリデーションを用いることでより安全に開発することができます。