Hono スタック
Hono は簡単なことを簡単にし、難しいことも簡単にします。 JSON を返す用途だけでなく、REST API サーバーとクライアントを含むフルスタックアプリケーションの構築にも適しています。
RPC
Hono の RPC 機能を使うと、少しのコード変更で API 定義を共有できます。 hc が生成するクライアントは定義を読み取り、型安全にエンドポイントへアクセスします。
次のライブラリを組み合わせて実現します。
- Hono — API サーバー
- Zod — バリデーター
- Zod バリデーターミドルウェア
hc— HTTP クライアント
これらのコンポーネントの組み合わせを Hono スタックと呼ぶことができます。 このスタックで API サーバーとクライアントを作ってみましょう。
API の実装
まず、GET リクエストを受け取り、JSON を返すエンドポイントを実装します。
import { Hono } from 'hono'
const app = new Hono()
app.get('/hello', (c) => {
return c.json({
message: `Hello!`,
})
})Zod によるバリデーション
Zod でクエリパラメーターを検証し、その値を取得します。

import { zValidator } from '@hono/zod-validator'
import * as z from 'zod'
app.get(
'/hello',
zValidator(
'query',
z.object({
name: z.string(),
})
),
(c) => {
const { name } = c.req.valid('query')
return c.json({
message: `Hello! ${name}`,
})
}
)型の共有
エンドポイントの型をエクスポートして、API 定義を公開します。
注意
RPC がルートを正しく推論できるように、対象となるすべてのメソッドをチェーンで呼び出し、宣言した変数からエンドポイントまたはアプリケーションの型を推論する必要があります。詳しくは RPC のベストプラクティスをご覧ください。
const route = app.get(
'/hello',
zValidator(
'query',
z.object({
name: z.string(),
})
),
(c) => {
const { name } = c.req.valid('query')
return c.json({
message: `Hello! ${name}`,
})
}
)
export type AppType = typeof routeクライアント
次に、クライアント側を実装します。 AppType をジェネリック型引数として hc に渡し、クライアントオブジェクトを作ります。 すると、エンドポイントのパスやリクエスト型が自動補完されるようになります。

import type { AppType } from './server'
import { hc } from 'hono/client'
const client = hc<AppType>('/api')
const res = await client.hello.$get({
query: {
name: 'Hono',
},
})返される Response は fetch API と互換性があり、json() で取得するデータにも型が付いています。

const data = await res.json()
console.log(`${data.message}`)API 定義を共有することで、サーバー側の変更をクライアントでも把握できます。

React との組み合わせ
React を使って Cloudflare Workers 向けのアプリケーションを作れます。
API サーバーの実装です。
// src/index.ts
import { Hono } from 'hono'
import * as z from 'zod'
import { zValidator } from '@hono/zod-validator'
const schema = z.object({
id: z.string(),
title: z.string(),
})
type Todo = z.infer<typeof schema>
const todos: Todo[] = []
const api = new Hono()
.post('/todo', zValidator('form', schema), (c) => {
const todo = c.req.valid('form')
todos.push(todo)
return c.json({
message: 'created!',
})
})
.get('/todo', (c) => {
return c.json({
todos,
})
})
export type AppType = typeof api
const app = new Hono()
app.route('/api', api)
export default appReact と React Query を使ったクライアントです。
// src/App.tsx
import {
useQuery,
useMutation,
QueryClient,
QueryClientProvider,
} from '@tanstack/react-query'
import type { AppType } from '../functions/api/[[route]]'
import { hc, InferResponseType, InferRequestType } from 'hono/client'
const queryClient = new QueryClient()
const client = hc<AppType>('/api')
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<Todos />
</QueryClientProvider>
)
}
const Todos = () => {
const query = useQuery({
queryKey: ['todos'],
queryFn: async () => {
const res = await client.todo.$get()
return await res.json()
},
})
const $post = client.todo.$post
const mutation = useMutation<
InferResponseType<typeof $post>,
Error,
InferRequestType<typeof $post>['form']
>({
mutationFn: async (todo) => {
const res = await $post({
form: todo,
})
return await res.json()
},
onSuccess: async () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
onError: (error) => {
console.log(error)
},
})
return (
<div>
<button
onClick={() => {
mutation.mutate({
id: Date.now().toString(),
title: 'Write code',
})
}}
>
Add Todo
</button>
<ul>
{query.data?.todos.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
</div>
)
}