@reiwa

HonoのHCで外部APIのクライアントを作る

typescript
hono

HackerNewsのAPIのリクエストとレスポンスを型安全にする方法を考えます。少なくともAnyのまま扱うよりは安全な開発ができます。

https://github.com/HackerNews/APIhttps://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)
}

このようにエンドポイントが補完されます。

img

URLの?print=prettyの部分も補完されます。

img

HCを使ってみる

HCはHonoのRPCとなるライブラリです。

https://hono.dev/docs/guides/rpchttps://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)
}

ここで関数hcfetchを渡してあげる必要があります。

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/validationhttps://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を取り扱うことはよくあります。
そのような場合は、型補完やバリデーションを用いることでより安全に開発することができます。

その他のタグ

関連する記事