この記事はトグルホールディングス(以下、トグル)エンジニアアドベントカレンダーの8日目の記事です!
自己紹介
こんにちは、トグルのエンジニアをしている宮崎です。
私は新卒で営業職としてキャリアをスタートしましたが、第二新卒としてエンジニアに転向しました。前職ではSES企業で機械学習エンジニアとして統計学やAIの学習に取り組みましたが、なかなか成果を形にできず、試行錯誤を重ねていました。
現在はトグルで活動しており、配属当初は業務効率化を目的とした開発を担当していました。徐々にアウトプットを出せるようになり、自分の成長を実感しています。
エンジニアとしての実務経験は約1年半ですが、現在はプロダクトエンジニアとしてフロントエンドとバックエンドの両方に携わり、主にTypeScriptを使用した開発を担当しています。これまで非常に多くの方々に支えられながら経験を積むことができ、これからも幅広い技術領域でスキルを磨き、さらなる成長を目指していきます。
導入
私はデベNABIという不動産開発用地仕入の生産性を「爆上げ」するオンラインサービス開発をしています。
今回の記事では、 Hono を用いた開発で直面した問題点とその解決策についてお話ししたいと思います。
弊社では、 Web フレームワークに Hono を採用しています。
「Hono」の開発者として知られる和田裕介(@yusukebe)さんが公演内容をまとめた「Honoの来た道とこれから」 でもご紹介いただき光栄に感じています。
Hono の開発で欠かせないルーティング機能。しかし、新たに API を追加した際に、フロントエンドからのリクエストが正しく処理されない問題に直面することがあります。
そこで今回は、私が Hono を用いた開発で感じた、つまずきやすいポイントとその対処方法を紹介します。(自戒を込めて)
弊社では** HTTPクライアントを生成するためのライブラリ**は Zodios
を用いております。他にも Axios
や Fetch API
、 GraphQL
などがありますが、今回は Zodios
を使用する前提で執筆しました。
フロントエンドからのリクエストが処理されない原因と解決策
1. フロントエンドと HTTPクライアントの設定不一致
-
method
、response
、Pathパラメータ
、alias
の不一致。既存のAPI定義などコピーをするなどして起きてしまう見落としです。(大抵これ...) - 特に注意が必要なのが** Pathパラメータの定義**です。以下のサンプルコードを見てください。
弊社では、フロントエンドはキャメルケース、バックエンドはスネークケースで書いています。これは、 DB (PlanetScale
)の制約上、キャメルケースに対応していないためです。
// フロントエンドでは、スネークケースで指定する必要がありますが、これがキャメルケースと混同されやすい原因となります。
// APIエンドポイントの定義と、データ取得のためのルート構築処理の一部
export const apiMemo = apiBuilder({
method: 'get',
path: '/:property_id',
alias: 'getMemoAndPropertyFileList',
description: 'Get memo list of the property',
parameters: [
{
name: 'property_id', // スネークケース
description: 'property ID',
type: 'Path',
schema: z.string().uuid(),
},
],
response: memoAndPropertyFileListResponseSchema,
})
// フロントエンド
import { usePropertyManagementApiClient } from '@/hooks/usePropertyManagementApi'
import useSWR from 'swr'
export default function useGetMemoList(props: { propertyId: string }) {
const { propertyId } = props
const apiClient = usePropertyManagementApiClient()
const fetcher = () => apiClient.getMemoAndPropertyFileList({ params: { property_id: propertyId } }) // スネークケースに揃えて定義
const { data } = useSWR(fetcher, {
revalidateOnFocus: false,
})
return data
}
// バックエンド
static apply(app: Hono) {
app.get('/memo/:property_id', getMemoAndPropertyFileList) // スネークケース
}
そのためフロントエンド側で
const fetcher = () => apiClient.getMemoAndPropertyFileList({ params: { property_id: propertyId } }) // スネークケースに合わせて定義
のように書いて定義を合わせています。このときに混乱して不一致となることがあります。
1つ1つ定義を確認する必要があります。
2. リニアなルーター(LinearRouter
)による意図しないエンドポイントの呼び出し
例えば、以下のようなバックエンドのAPI定義がある場合を考えます。
// バックエンドでのAPIの定義
static apply(app: Hono) {
app.get('/memo/:property_id', getMemo)
app.get('/memo/file/:property_id', getMemoAndPropertyFile)
}
リクエスト:
GET /memo/file/12345
のとき、 :propertyId
は動的パラメータとして定義されており、 file/12345
全体を :propertyId
として解釈します。
結果的に想定外の getMemo
関数が呼び出されてしまいます。
これは「登録されたルート情報があり、それを来たリクエストのパスに応じて頭からマッチする」ためです。これが LinearRouter
の特性です。
具体的な対策
a. ルートの順序を変更
より具体的なルートを先に定義します。
app.get('/memo/file/:propertyId', MemoHandler.getMemoAndPropertyFile); // より具体的
app.get('/memo/:propertyId', MemoHandler.getMemo); // より抽象的
これにより、 GET /memo/file/12345
は最初のルート /memo/file/:propertyId
にマッチし、正しく getMemoAndPropertyFile
関数が呼び出されます。
b. 正規表現ベースのルーターを導入する
正規表現ベースのルーター(例: Hono
の RegExpRouter
)を使用すると、ルートの具体性に応じて正しいルートを選択します。この場合、 /memo/file/:propertyId
は /memo/:propertyId
よりも具体的と見なされ、正しく動作します。
RegExpRouter とは?
- リクエストのパスに対して正規表現を使った柔軟で効率的なマッチングを行うルーター
- 具体的には、動的なPathパラメータや複雑なルート定義が含まれる場合に、最も具体的なルートを正確に選択して処理を行えるようになります!
RegExpRouter
は現在採用していないが Hono の Router から比較的簡単に導入できそう。
ルーターの選定
RegExpRouter
が万能の印象がありますが、 LinearRouter
はルーティングパスの登録を非常に素早く行います。他の JavaScript の高速なルーターと比べても速いです。
RegExpRouter
は早いルーターの一つですが、ルーティングパスを登録するのが少し遅いです。複雑なパスや動的パラメータに対応し、柔軟かつ効率的なマッチングを提供しますが、その分リソースを多く消費します。
まとめ
ドキュメント通りに実装しても、実際に触ってみるとつまずくことはよくあります。
特にルーティングに関して重点的に触れましたが、 RegExpRouter
などを活用することで仕組み化が進み、解決できると思います。ただし、ルーター選定にはパフォーマンスの違いがあるため、開発環境や目的に応じて吟味する必要があります。